Skip to Content
Platform
How-toAsk the user mid-task

Ask the user mid-task

The agent stops, your app shows a question or an Approve button, and the task continues with the answer.

An interrupt pauses a task until its owner answers a question or approves an action. When someone else approves, or the user is in Slack, have the agent draft instead.

You need: a recipe you can change (Add a tool) and an app that runs tasks (Add a chat to your web app).

1. Define the ask tools

extensions/ask-user.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { askUserApproval, askUserQuestion } from "@introspection-ai/recipes/interactions"; import { Type } from "typebox"; export default function (pi: ExtensionAPI) { pi.registerTool({ name: "ask_user_question", label: "Ask user", description: "Ask the user a clarifying question and wait for the answer.", parameters: Type.Object({ question: Type.String(), options: Type.Optional(Type.Array(Type.String())) }), executionMode: "sequential", async execute(toolCallId, p, signal, _onUpdate, ctx) { return await askUserQuestion({ question: p.question, options: p.options }, { toolCallId, ctx, signal }); }, }); pi.registerTool({ name: "request_approval", label: "Request approval", description: "Ask the user to approve an action before taking it.", parameters: Type.Object({ title: Type.String(), body: Type.String() }), executionMode: "sequential", async execute(toolCallId, p, signal, _onUpdate, ctx) { return await askUserApproval({ title: p.title, body: p.body }, { toolCallId, ctx, signal }); }, }); }

Add both to the agent’s tools. Questions also take selection: "multiple".

2. Detect the pause in your app

The run ends with an interrupt outcome and the task moves to awaiting_user.

let interrupts = []; for await (const e of handle.stream()) { if (e.type === EventType.RUN_FINISHED && e.outcome?.type === "interrupt") interrupts = e.outcome.interrupts; }

After a reload, read metadata.pending_interrupts from tasks.get. Each has an id (send it back unchanged), a reason, a message, and metadata.

3. Resume with the answer

The task stays awaiting_user until every pending interrupt has an answer.

await runner.tasks.runs.resume(taskId, { resume: [{ interruptId: interrupts[0].id, status: "resolved", payload: { answer: "EU" } }], });
Interruptstatuspayload
Questionresolved{ "answer": "EU" }, or { "answers": [...] } for multiple
Approvalresolved{ "approved": true } or { "approved": false, "feedback": "Lower it" }
Dismiss eithercancellednone

Resume with the same credential that owns the task.

Verify

introspection tasks get <task-id> --query 'metadata.pending_interrupts' introspection tasks resume <task-id> --interrupt-id <id> --payload '{"approved": true}' introspection tasks get <task-id> --query status -o table

Expect the pending list, then running or idle.

Or ask your agent: “Add an approval step before any refund, with buttons in our chat UI.”

How long a pause lasts

AfterWhat happens
10 minutes idleThe sandbox is torn down. Files and memory are saved.
3 hours with no activityThe task stops holding a concurrency slot.
Any time laterResume or prompt it; the conversation is intact.

An expiresAt on a question is only recorded: cancel stale ones with tasks resume <task-id> --cancel-interrupt <id>.

Limits

Not supportedDo this instead
Questions from a subagentOnly the root agent pauses: ask from it.
Interrupts in SlackAsk in the thread with reply.
A webhook when a task pausesKeep the stream open, or poll tasks list --status awaiting_user.
Last updated on