.agents/skills/piece-output-schema/SKILL.md
An outputSchema turns a step's raw JSON output into a friendly, typed, labelled tree in the flow builder's data selector and output viewer — and a path map that LLM/MCP consumers use to find the fields that matter. This skill takes a piece from "raw JSON dump" to curated schemas across all its actions and triggers.
Read a shipped example before starting: packages/pieces/community/clickup/src/lib/output-schemas.ts is the richest; google-docs and google-calendar are readable smaller ones.
An outputSchema is a curated tree: at every level you describe, only the fields you list appear — their siblings are dropped. That is exactly how you keep the output clean.
schema.fields render; undescribed root siblings are gone. Inside a described object (children) or array item (listItems), only the children you list render — the resolved value's other keys are dropped.children/listItems), the renderer drills the whole value generically (matrices → Row/Cell, arrays → list, objects → every key). Describe a container's useful inner fields, or leave the field off entirely — there is no way to name a container and show only some of its contents without listing them.datetime, url, email, boolean, image, filesize, html, number, date, currency, duration) where one fits, and record the path so data selector and AI/MCP consumers can find it.Because the schema describes what the action's run() returns (not the raw third-party API response), you must know the return shape before you can map paths. See capture-recipes.md.
npm start / npm run dev). Dev pieces load from each piece's built dist/ — see capture-recipes.md if a piece doesn't appear.AP_DEV_PIECES.Ask the user for the piece(s) and the connection to use before starting.
List the piece's actions and triggers (packages/pieces/community/<piece>/src/lib/{actions,triggers}). For each, decide whether it gets a schema using the table below.
| Step kind | Schema? |
|---|---|
| Create / Update / Get / Read / List / Search / Find | Yes |
Delete / clear / archive that returns an empty body ({}, '', 204) | No — nothing to describe |
custom_api_call (generic passthrough) | No |
| Polymorphic trigger (payload is message OR poll OR callback, etc.) | No — a single shape would mislabel the others (e.g. Telegram "New Update") |
| Webhook / polling trigger with a stable payload | Yes — describes ONE item (the per-run payload) |
Open the action/trigger's run() (and test() for triggers). Note whether it returns response.body, response.data, the full HTTP/Gaxios wrapper ({status, headers, body, config}), or a hand-built/transformed object. Never surface config or headers — config.headers.Authorization leaks the bearer token. The schema's top-level value paths are relative to this returned object.
Run each step against the live connection and capture the exact output JSON. Full recipes in capture-recipes.md.
POST /v1/sample-data/test-step API once a flow with the step exists. Running the piece's own code delivers faithful output and lets the engine refresh OAuth tokens for you.[].Write the schema in packages/pieces/community/<piece>/src/lib/output-schemas.ts (create the file if absent). Full field reference, formats, labels, and wiring in schema-reference.md. The essentials:
value (the path) is optional and defaults to key. For a plain field, set key to the real JSON property name and omit value (the dominant shipped style); set value only to unwrap (body.*, data.*) or rename. See key vs value.format to every field where one fits (datetime, url, email, boolean, image, filesize, html, number, date, currency, duration).children / listItems paths are RELATIVE to the parent's value — this is the #1 correctness bug. owners[].displayName is described as a top-level field owners with a listItems child { key: 'displayName' }, NOT a child path of owners.displayName.value: '' + listItems, plus a schema-level itemLabel template (e.g. 'Row {row}').dynamicKey: true.labelKey to lists/maps so items show a meaningful label; itemLabel for top-level arrays.taskFields, a Drive fileFields) into a const and reference it from every action/trigger that returns it.Resolve every field's value ?? key against the captured JSON at the correct scope — top-level against the root, children against the parent object, listItems against one array item. A path that doesn't resolve is a dead field; re-capture if the shape is ambiguous. For a piece with many schemas, verify each one adversarially — one sub-agent per schema, given only the schema and its captured payload, asked to find any path that fails to resolve.
outputSchema: <name> to each action/trigger object (or populate the trigger registration map — see schema-reference.md).package.json (every touched piece) — this is what forces cloud/self-hosted registries to re-ingest the fresh metadata.npm run lint-dev (or npx turbo run lint --filter=@activepieces/piece-<name>). Typecheck must be clean.children/listItems paths are relative; top-level array uses a value: '' wrapper + itemLabel.config, headers, tokens, or auth secrets appear in any schema.labelKey/itemLabel applied where they help.lint-dev green; friendly tree verified in the builder.outputSchema ships as part of the served piece metadata (dev pieces from dist/, published pieces from the registry). If a schema doesn't appear in the builder, confirm the piece's patch version was bumped and the piece was rebuilt + reloaded — that's the served metadata refreshing. Legacy servers older than #13983 stripped outputSchema during registry ingestion; irrelevant for current builds.
piece-builder skill — building pieces and the output-quality.md reference (shaping run() return values for table-readiness) complements this skill, which describes an existing return.