Skip to main content

Overview

When an agent needs human input — such as approval for a sensitive action, clarification on an ambiguous request, or additional data — the run enters the awaiting_input state. Your application is responsible for:
  1. Detecting the run.awaiting_input event.
  2. Presenting the prompt to a human operator.
  3. Collecting their response.
  4. Sending a signal to resume the run.

Signal types

SignalPurposeWhen to use
approveApprove the proposed action and continue executionThe agent’s plan looks correct
rejectReject the proposed action and fail the runThe agent’s plan is wrong or dangerous
submit_inputProvide additional input and continue executionThe agent needs more information

Detecting awaiting_input

When a run enters awaiting_input, the event stream emits a run.awaiting_input event:
{
  "seq": 21,
  "type": "run.awaiting_input",
  "timestamp": "2026-03-26T14:35:01.000Z",
  "payload": {
    "redacted": false,
    "value": {
      "request_id": "uuid",
      "reason_code": "AWAITING_APPROVAL",
      "input_kind": "approval"
    }
  }
}
The reason_code field describes why input is needed. The input_kind field indicates what type of signal is expected.

Sending a signal

Send a signal to POST /v1/runs/:id/signal:

Approve

curl -X POST https://api.agentharness.com/v1/runs/run_abc123/signal \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "action": "approve"
  }'

Reject

curl -X POST https://api.agentharness.com/v1/runs/run_abc123/signal \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "action": "reject",
    "payload": {
      "reason": "This action would email the wrong distribution list."
    }
  }'

Submit input

curl -X POST https://api.agentharness.com/v1/runs/run_abc123/signal \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "action": "submit_input",
    "payload": {
      "recipient": "team-leads@company.com",
      "subject": "Q1 Summary",
      "additional_context": "Only include announcements from March."
    }
  }'

Signal request schema

FieldTypeRequiredDescription
actionstringYesSignal action: approve, reject, or submit_input
payloadobjectNoAdditional data (used with reject reason or submit_input data)
idempotency_keystringNoBody-level idempotency key for safe retries

Complete example: human-in-the-loop flow

This example demonstrates a complete flow: create a run, monitor for awaiting_input, collect approval from a human, and handle the result.
import { fetchEventSource } from "@microsoft/fetch-event-source";
import * as readline from "readline";

const API_BASE = process.env.API_BASE ?? "https://api.agentharness.com";
const API_KEY = process.env.API_KEY!;

const createRes = await fetch(`${API_BASE}/v1/runs`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "idempotency-key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    input: { goal: "Draft and send the weekly team update email." },
  }),
});
const run = await createRes.json();
console.log(`Run created: ${run.id}`);

const ac = new AbortController();

await fetchEventSource(`${API_BASE}/v1/runs/${run.id}/events/stream`, {
  signal: ac.signal,
  headers: { Authorization: `Bearer ${API_KEY}` },
  async onmessage(msg) {
    if (msg.event !== "run_event" || !msg.data) return;
    const event = JSON.parse(msg.data);
    console.log(`[${event.type}] seq=${event.seq}`);

    if (event.type === "run.awaiting_input") {
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
      });
      const answer = await new Promise<string>((resolve) => {
        rl.question("Approve? (yes/no): ", resolve);
      });
      rl.close();

      const action = answer.toLowerCase() === "yes" ? "approve" : "reject";
      await fetch(`${API_BASE}/v1/runs/${run.id}/signal`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          "Content-Type": "application/json",
          "idempotency-key": crypto.randomUUID(),
        },
        body: JSON.stringify({ action }),
      });
      console.log(`Signal sent: ${action}`);
    }

    const terminal =
      event.type === "run.worker.succeeded" ||
      event.type === "run.worker.failed" ||
      event.type === "run.cancelled";
    if (terminal) {
      console.log(`\nRun ended: ${event.type}`);
      ac.abort();
    }
  },
});

Timeouts

If no signal is received within the configured timeout, the run is automatically cancelled. The event stream emits:
  1. run.cancelled with reason_code: "signal_timeout"
Design your application to handle timeouts gracefully — notify the operator and provide a way to create a new run if needed.

Error handling

ErrorReason codeCause
Run not awaiting inputrun_not_awaiting_inputSignal sent to a run not in awaiting_input
Invalid signal typeinvalid_signal_typeSignal action not in the accepted list
Run not foundrun_not_foundInvalid run ID