Skip to content
Français

Let an agent ask questions

Pause an agent task for a human answer and continue the saved conversation.

Use an interactive task when the agent needs to ask a person for information before continuing. Use an approval task when your workflow needs permission to proceed. These two pauses have different inputs and resume rules.

Interactive taskApproval gate
QuestionWritten by the agent, adapted to earlier answersThe fixed prompt of the gate
AnswerFree text, or one of the agent’s choicesapprove or reject
What it continuesThe agent’s conversation, in a new turnThe tasks that depend on the gate
Submitted withstart({ answers })start({ decisions })
Signed proofsNoOptional, with authentication: "signed"
DefinitiondefineInteractiveAgentTask()defineApprovalTask(), definePauseTask()

The task needs a checkpoint: it stores the questions and answers between processes.

import { defineInteractiveAgentTask } from "@elie-laloum/outpost";
import { repository, coder, sandboxProvider } from "./outpost.config.ts";

export const clarify = defineInteractiveAgentTask({
  key: "clarify",
  repository,
  agent: coder,
  sandboxProvider,
  brief:
    'Define the application with its owner, then complete with {"summary": string, "features": string[]}.',
  actors: ["owner"],
});
import {
  createWorkflowCheckpointStore,
  createLocalTransport,
} from "@elie-laloum/outpost";

export const store = createWorkflowCheckpointStore({
  transporter: createLocalTransport({ directory: ".outpost/storage" }),
});
import { reportValue } from "./reporter.ts";
import { defineWorkflow } from "@elie-laloum/outpost";
import { clarify } from "./clarify.ts";
import { store } from "./question-store.ts";

export const workflow = defineWorkflow("discovery", [clarify]);
export const checkpoint = { store, runId: "discovery-42", version: "1" };
export const result = await workflow.start({ checkpoint });
reportValue(result.status, result.inputRequests[0]?.question);
// Example output: waiting-input What should the new endpoint return?

It prints waiting-input and the agent’s first question. Outpost adds the question protocol to your brief, so the brief only describes the goal and the shape of the final JSON.

API reference: InteractiveAgentTaskOptions.

Codex, Claude Code, Copilot CLI, Kimi Code and the built-in harness are accepted. Antigravity, and a harness created with conversations: false, are rejected when the task is defined: see Conversations.

Drag to move · Ctrl + scroll to zoom
100 %
  • TurnThe agent works in a fresh sandbox.
    1. RunIt continues its conversation with the brief or the latest answer. sandbox
    2. CloseOutpost saves the conversation and closes the sandbox. host
    (Steps)
    • → Question : then
  • QuestionThe run stops with waiting-input.
    1. SaveThe checkpoint stores the question; dependent tasks wait. inputRequests
    (Steps)
    • → Answer : then
  • AnswerYour application submits it.
    1. ValidateOutpost checks and saves the answer, then starts the next turn. start({ answers })
    (Steps)
    • → Output : then
  • OutputThe agent completes with JSON instead of asking.
    1. KeepThe checkpoint stores the output. result.value()
    (Steps)

result.inputRequests lists every pending question. Independent interactive tasks can wait at the same time.

API reference: WorkflowInputRequest.

Restart the same workflow with the same checkpoint and an answers entry per request.

import type { WorkflowInputRequest } from "@elie-laloum/outpost";

export function answerValue(
  request: WorkflowInputRequest,
  actor: string,
  value: string,
) {
  return {
    executionId: request.executionId,
    key: request.key,
    requestId: request.id,
    actor,
    value,
  };
}
import type {
  Workflow,
  WorkflowCheckpointOptions,
  WorkflowInputRequest,
} from "@elie-laloum/outpost";
import { answerValue } from "./answer-value.ts";

export async function answer(
  workflow: Workflow,
  checkpoint: WorkflowCheckpointOptions,
  request: WorkflowInputRequest,
  actor: string,
  value: string,
) {
  return workflow.start({
    checkpoint,
    answers: [answerValue(request, actor, value)],
  });
}

start() runs the next turn and returns at the next question or once the task ends. Called without answers, it returns the pending questions without calling the model.

Outpost checks every answer before applying any. It rejects a stale requestId, an actor outside actors, another execution, a second answer for the same task and, when allowFreeText is false, a value outside choices.

Read the completed dialogue with result.value(clarify).

API reference: InteractiveAgentResult.

result.usage adds up the attempts and tokens of every turn, repairs included. unwrap() throws while the run waits. In a workflow that also has gates, waiting-input wins over paused, and a failure or cancellation wins over both.

Each turn opens a sandbox and closes it before the question is published. Files in the worktree carry over between turns, committed or not; the sandbox home and running processes do not.

Outpost never integrates, pushes or deletes the worktree. Review branch and merge it yourself (Repository and branch), then prune it with Retention and cleanup.

The repository, the worktree and the conversation store must stay at the same paths for the next process. A moved worktree fails with Interactive workspace moved; recover it explicitly, and a switched branch with Interactive workspace branch changed; recover it explicitly.

A crash during a turn leaves the task incomplete, and the next start() refuses to replay it. Authorize the replay with checkpoint: { ...checkpoint, resume: "retry-incomplete" }.

The turn restarts from the last saved conversation and answer; a completed output is reused without calling the model. Partial effects of the interrupted turn may repeat. Durable runs covers replay and recovering the checkpoint’s ownership.

defineTask() with interaction suspends any task on a question. defineInteractiveAgentTask() is built on it.

import { defineTask } from "@elie-laloum/outpost";

const region = defineTask({
  key: "region",
  interaction: { identity: "region-v1", actors: ["owner"] },
  perform: (context) => {
    const interaction = context.interaction;
    if (!interaction) throw new Error("Run with a checkpoint");
    const answer = interaction.answer;
    if (!answer)
      return interaction.suspend(
        { question: "Deploy to which region?", choices: ["eu", "us"] },
        { step: "region" },
      );
    return { region: answer.value };
  },
});

perform runs again from the start after each answer. Read interaction.state to skip finished work, and save(state) to record progress; both hold JSON only.

  • A question on the last of maxTurns fails the task instead of waiting.
  • Questions do not expire, and answers are not signed.
  • Cancelling stops a running turn; a question already saved stays pending.
  • Changing the agent, model, brief, repository, provider, actors or maxTurns makes the saved checkpoint incompatible: start a new runId.

A complete scenario with an approval, red tests and reviewed code: Build a development workflow.

API: defineInteractiveAgentTask · InteractiveAgentTaskOptions · InteractiveAgentResult · WorkflowInputRequest · WorkflowAnswer · TaskInteractionContext