Skip to Content
Platform
How-toGet structured output

Get structured output back

Your code gets a typed object, not prose to parse.

There is no response_format or schema option on tasks or agents. Instead, give the agent a submit tool whose parameters are your schema, and read the arguments it was called with. Use this when a program, not a person, consumes the result.

You need: a recipe you can change (Add a tool) and a runtime you call from code (Run tasks from your backend).

1. Define the submit tool

Pi validates every call against the TypeBox schema and returns errors to the model, which corrects the call.

extensions/submit-result.ts
import { type ExtensionFactory, defineTool } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; export const Triage = Type.Object({ category: Type.Union([Type.Literal("billing"), Type.Literal("bug"), Type.Literal("other")]), priority: Type.Integer({ minimum: 1, maximum: 4 }), summary: Type.String({ maxLength: 280 }), }); const submitResult = defineTool({ name: "submit_result", label: "Submit result", description: "Submit the final triage. Call exactly once, as your last action.", parameters: Triage, execute: async () => ({ content: [{ type: "text" as const, text: "Saved." }], details: {} }), }); const extension: ExtensionFactory = (pi) => pi.registerTool(submitResult); export default extension;

2. Allow the tool and require it

agents/agent.yaml
tools: [read, submit_result] system_instructions: mode: append content: | Finish every task by calling submit_result exactly once. Do not put the result in prose.

3. Read the arguments from the stream

const handle = await runner.tasks.start({ prompt: "Triage ticket 4411: ..." }); const args = new Map<string, string>(); let submitId: string | undefined; for await (const e of handle.stream()) { if (e.type === EventType.TOOL_CALL_START && e.toolCallName === "submit_result") submitId = e.toolCallId; if (e.type === EventType.TOOL_CALL_ARGS) args.set(e.toolCallId, (args.get(e.toolCallId) ?? "") + e.delta); } const raw = submitId ? args.get(submitId) : undefined;

Later, read it from the conversation instead (query under Verify).

4. Validate, and ask once more if needed

import { Value } from "typebox/value"; const result = raw ? JSON.parse(raw) : undefined; if (!Value.Check(Triage, result)) { await runner.tasks.runs.create(taskId, { kind: "prompt", prompt: { text: "You did not call submit_result with a valid triage. Call it now." }, }); }

Require it, don’t just measure it

A judge measures how often answers cite sources; it can’t make them. To require a field, put it in the schema, for example sources: Type.Array(Type.String(), { minItems: 1 }), and show users only the submit tool’s fields.

Verify

introspection tasks create --runtime support-agent --environment staging \ --prompt "Triage: customer charged twice for one order" introspection conversations get <task-id> -o json \ --query "items[?attributes.gen_ai.tool.name=='submit_result'].attributes.gen_ai.tool.call.arguments"

Expect one JSON string that matches the schema.

Or ask your agent: “Make support-agent return its triage through a submit_result tool.”

Limits

ProblemDo this
No submit_result callAdd it to the agent’s tools and require it in the instructions.
Several callsTake the latest, and say “exactly once”.
A downstream system rejects the result laterStore the task ID with the record and send a tracked event (Recurring problems and feedback).
Last updated on