Skip to Content
Platform
How-toDraft, then approve

Draft, then approve

The agent never sends. A person says yes, your system acts, and the agent hears about it.

A draft is the agent’s proposed action, saved for a person to send or discard. Use it when someone other than the task owner approves. Your recipe enforces it, not the platform.

You need: a recipe you can change (Add a tool) and a backend that starts tasks (Run tasks from your backend).

1. Keep sending out of the recipe

List only read tools in each MCP server’s tools.include, such as ["<app>-find-email", "<app>-get-thread"]; a tool not listed never reaches the agent.

2. Give the agent a draft tool

extensions/save-draft.ts
import { type ExtensionFactory, defineTool } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; const saveDraft = defineTool({ name: "save_draft", label: "Save draft", description: "Save an email for a person to send or discard. You cannot send it.", parameters: Type.Object({ id: Type.String(), to: Type.Array(Type.String()), subject: Type.String(), body: Type.String() }), execute: async (_id, p) => ({ content: [{ type: "text" as const, text: `Draft ${p.id} saved for review.` }], details: {} }), }); const extension: ExtensionFactory = (pi) => pi.registerTool(saveDraft); export default extension;

Add save_draft to the agent’s tools. Your backend reads its arguments (structured output) and stores each draft by id.

3. Notify the approver

Your backend shows Send and Discard, or notifies the reviewer by Slack, email, or a queue. The approver needs no Introspection account.

4. Tell the agent what happened

Post the outcome into the task as a steer. Tell the agent in SYSTEM.md that a <user_action> tag records what a person did and is never a request.

await runner.tasks.runs.create(taskId, { kind: "steer", prompt: { text: '<user_action kind="draft" action="sent" id="d-17" by="maria">Sent text</user_action>' }, });
Task started withSteer it with
API key + user_id or anonymous_idA runner opened with the same identity
API key + conversation_id onlyAny API key for the same agent member, with no identity
App sign-in or SlackAn Admin Agent key, with no identity

Verify

introspection conversations get <task-id> \ --query "length(items[?attributes.gen_ai.tool.name=='save_draft'])" -o table

Expect 1 or more, then a later run that acknowledges the outcome. Keep the decision record in your system; a reviewer can also mirror it with introspection annotations create from their own login (annotate).

Or ask your agent: “Make support-agent draft refunds over the limit instead of issuing them.”

Approve in Slack (tasks started in Slack)

The agent asks in the thread and passes the replier’s from ID (who wrote each message) to a tool that checks your allow-list. That ID is attribution, not proof: for a hard gate, your system executes the draft. Tasks started elsewhere can’t post to Slack: post the draft with your own Slack app and steer the task on a click.

Gate by amount

To make the agent draft above a limit, use the fail-closed extensions/refund-limit.ts hook and REFUND_LIMIT binding from Block a tool call.

Limits

Not supportedDo this instead
A hosted review queue for draftsYour app or ticket system holds the queue.
A webhook when a task needs approvalKeep the stream open, or poll tasks get.
Last updated on