> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Active-horizon agents

> A durable supervisor for agents that keep working for minutes or hours — budgets, steering, and a bounded ledger that survives continue-as-new.

Most agent loops live inside one request. `ActiveHorizonAgentWorkflow` is the
durable supervisor for an agent that should keep making progress for **minutes
or hours** — long past any HTTP timeout, any process restart, and any single
model call.

It is not an open-ended autonomous process. Every turn runs as a bounded child
workflow inside four budgets, against an explicit allowlist of workflow types,
and an operator can steer or stop it at any point.

<Note>
  The supervisor decides *what runs next*; it does not execute anything itself.
  Each turn is a child workflow — commonly
  [`SandboxAgentLoopWorkflow`](/stateset-sandbox/stateset-sandbox-agent-sessions),
  which runs the work inside an isolated sandbox, but any allowlisted workflow
  type can serve as a turn.
</Note>

## The four budgets

An agent stops when it finishes, or when it hits the first budget to run out.
Every value defaults if you omit it, and every value is clamped — an agent
cannot be configured to run unbounded.

| Budget           | Field                | Default   | Range                   |
| ---------------- | -------------------- | --------- | ----------------------- |
| Wall-clock       | `active_window_secs` | 1 hour    | 5 minutes – 24 hours    |
| Turns            | `max_turns`          | 50        | 1 – 500                 |
| Failures         | `max_failures`       | 5         | 1 – 100                 |
| Per-turn timeout | `turn_timeout_secs`  | 5 minutes | 30 seconds – 30 minutes |

Elapsed time is measured with Temporal's replay-safe workflow clock, not the
host clock, so a worker restart neither loses nor invents elapsed time.

## Starting one

```bash theme={null}
curl --request POST "$ENGINE_API/v1/workflows/active-horizon-agent/start" \
  --header "Authorization: Bearer $ENGINE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "brand_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "request_id": "9c1f0a2e-59a4-4d0b-9f2e-1b7a3c5d8e01",
    "goal": "Clear the delivery-exception backlog for orders shipped last week.",
    "tasks": [
      {
        "task_id": "t-1",
        "description": "Re-check tracking for the 40 oldest exceptions",
        "workflow_type": "DeliveryExceptionCallbackWorkflow",
        "payload": { "cohort": "shipped-2026-08-18", "limit": 40 }
      }
    ],
    "allowed_workflow_types": [
      "DeliveryExceptionCallbackWorkflow",
      "SandboxAgentLoopWorkflow"
    ],
    "active_window_secs": 7200,
    "max_turns": 120
  }'
```

`POST /v1/workflows/active-horizon-agent/validate` takes the same body and
checks it without starting anything — worth doing in CI for any recipe you
ship.

<Warning>
  `allowed_workflow_types` is a security boundary, not a convenience. A planner
  may add tasks while the agent runs, and explicitly queued tasks are checked
  against the same list — so a planner that returns something unexpected cannot
  launch an arbitrary registered workflow. Leave it empty and nothing dynamic
  can run at all, which is the safe default rather than the permissive one.
</Warning>

## Tasks and the planner

The agent works a queue. Each task names a child workflow and its input:

```json theme={null}
{
  "task_id": "t-7",
  "description": "Draft replacement approvals for the three oldest claims",
  "workflow_type": "SandboxAgentLoopWorkflow",
  "payload": { "prompt": "…", "sandbox": { "cpus": 2, "memory": "4Gi" } }
}
```

When the queue drains, the optional `planner` connector step runs and is
expected to return either `{ "done": true }` or `{ "task": … }` — so the agent
either finishes or gets its next piece of work. Without a planner, the agent
stops when the queue empties.

## The ledger is the memory

The supervisor continues as new every `checkpoint_every_turns` turns (default
10, clamped 1–50) so Temporal history stays bounded. What crosses that boundary
is a compact **ledger** — one entry per completed task — rather than raw
transcripts.

That is a deliberate constraint: the ledger has to be small enough that a fresh
generation can resume from it alone. If an agent needs detail from turn 3 at
turn 90, that detail belongs in the ledger entry or in durable storage, not in
a transcript the next generation will never see.

## Steering a running agent

Every control is a signal against a running workflow, so none of them race with
a turn in flight.

| Control       | Endpoint                               | Effect                                           |
| ------------- | -------------------------------------- | ------------------------------------------------ |
| Status        | `GET .../{workflow_id}/status`         | Goal, current task, generation, budgets consumed |
| Pause         | `POST .../{workflow_id}/pause`         | Finish the current turn, then hold               |
| Resume        | `POST .../{workflow_id}/resume`        | Continue from where it paused                    |
| Steer         | `POST .../{workflow_id}/steer`         | Add a note the agent sees on its next turn       |
| Enqueue       | `POST .../{workflow_id}/enqueue`       | Append a task to the queue                       |
| Reprioritize  | `POST .../{workflow_id}/reprioritize`  | Move task ids to the front, in order             |
| Extend budget | `POST .../{workflow_id}/extend-budget` | Add window, turns or failures                    |
| Cancel        | `POST .../{workflow_id}/cancel`        | Stop cleanly after the current turn              |
| Terminate     | `POST .../{workflow_id}/terminate`     | Emergency stop, no cleanup                       |

```bash theme={null}
# Nudge a running agent without restarting it
curl --request POST "$ENGINE_API/v1/workflows/active-horizon-agent/$WORKFLOW_ID/steer" \
  --header "Authorization: Bearer $ENGINE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "note": "Skip anything already refunded — finance handled those manually." }'
```

<Tip>
  Prefer `cancel` to `terminate`. Cancel lets the current turn finish and the
  ledger close out, so the run is still readable afterwards; terminate drops the
  workflow where it stands and leaves the last turn's outcome unrecorded. Keep
  terminate for the case where a turn is stuck and you cannot wait.
</Tip>

## Governed defensive-security recipes

A recipe whose `recipe_key` starts with `cyber-` runs under a stricter contract
than any other agent on the platform. The intent is bounded **defensive** work —
enriching an alert, collecting evidence, reviewing a configuration — under an
authorization that a customer has actually granted, with an expiry.

Every one of these is enforced by the API before the workflow starts, and the
whole scope is hash-bound to the run:

* `authorization_scope` is **required**, and is rejected on any non-`cyber-` recipe.
* `engagement_id` names the engagement, incident or change ticket the work is authorized under.
* `authorized_assets` must list 1–100 bounded selectors. `*`, `0.0.0.0/0` and `::/0` are rejected — there is no way to authorize everything.
* `permitted_actions` is a closed allowlist: `alert_enrichment`, `evidence_collection`, `configuration_review`, `vulnerability_validation`, `remediation_planning`. Nothing else is accepted, and duplicates are rejected.
* `expires_at` must be in the future, no more than 30 days out, and must cover the **entire** requested active window — an agent cannot outlive its authorization.
* Tasks may only launch `SandboxAgentLoopWorkflow`, so the work runs isolated.
* Shell and bootstrap commands are prohibited outright; the bounded planner passes direct argv.
* Executables are restricted to a read-only set: `pwd`, `rg`, `jq`, `head`, `tail`, `wc`, `sort`, `cat`, `stat`, `sha256sum`.

```json theme={null}
{
  "recipe_key": "cyber-alert-triage",
  "recipe_version": 3,
  "authorization_scope": {
    "engagement_id": "INC-2026-0814",
    "authorized_assets": ["web-01.prod.example.com", "web-02.prod.example.com"],
    "permitted_actions": ["alert_enrichment", "evidence_collection"],
    "expires_at": "2026-09-05T00:00:00Z"
  }
}
```

<Warning>
  The authorization scope is a customer's grant, not a configuration default.
  Widening `authorized_assets`, adding an action, or extending `expires_at`
  requires a new validated release — the scope is hash-bound, so an in-flight
  agent cannot have its authority broadened underneath it. If a run needs more
  reach than it was granted, it stops; that is the design, not a failure.
</Warning>

## How a run ends

`status` on the final result says which budget ended it, which is the first
thing to look at when an agent stops earlier than expected:

| Status                     | Meaning                                         |
| -------------------------- | ----------------------------------------------- |
| `completed`                | The queue drained and no planner was configured |
| `planner_done`             | The planner returned `{ "done": true }`         |
| `time_budget_exhausted`    | `active_window_secs` ran out                    |
| `turn_budget_exhausted`    | `max_turns` reached                             |
| `failure_budget_exhausted` | `max_failures` reached                          |
| `cancelled`                | An operator cancelled it                        |

A `failure_budget_exhausted` run is the one worth reading the ledger for: it
means turns were failing faster than the agent could make progress, and the
ledger entries name which.

## Next steps

<CardGroup cols={2}>
  <Card title="Sandbox agent sessions" icon="box" href="/stateset-sandbox/stateset-sandbox-agent-sessions">
    The isolated execution side — where a turn's work actually runs.
  </Card>

  <Card title="ResponseAutomationV2" icon="diagram-project" href="/next-temporal/workflow">
    The single-ticket workflow, and the contrast with a supervised agent.
  </Card>

  <Card title="Policy engine" icon="scale-balanced" href="/next-temporal/policy-engine">
    The verified-decision gate a mutating turn passes through.
  </Card>

  <Card title="Operations" icon="gauge-high" href="/next-temporal/operations">
    Running the engine, and what to watch while an agent is live.
  </Card>
</CardGroup>
