> For the complete documentation index, see [llms.txt](https://docs.interactive.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.interactive.ai/agents/guides/authoring-autonomous-routines.md).

# Authoring autonomous routines

> **Context** — Assumes [Autonomous routines](/agents/concepts/autonomous-routines.md) (the run lifecycle and failure taxonomy) and [Authoring routines](/agents/guides/authoring-routines.md) (node craft — it all applies here). This guide builds one automation end to end: a KYC decision processor.
>
> YAML examples follow **manifest schema 6.1.1**. Manifest and content shapes are schema-versioned and differ across runtime versions — see [Versioning & compatibility](/agents/operations/versioning.md).

## What we're building

A third-party KYC provider finishes verifying an applicant. We want the agent to assess the result, decide approve / reject / escalate, and deliver the decision to our backend — no human, no conversation.

## 1. Design the contract first

The schemas *are* the interface your callers code against. Write them before the nodes:

```yaml
autonomous:
  input_schema:
    type: object
    required: [applicant_id, verification_result]
    properties:
      applicant_id:
        type: string
      verification_result:
        type: object
        required: [status]
        properties:
          status:
            enum: [GREEN, AMBER, RED]
          checks:
            type: array
            items:
              type: object
  output_schema:
    type: object
    required: [decision, explanation]
    properties:
      decision:
        enum: [approved, rejected, escalate]
      explanation:
        type: string
  timeout_seconds: 60
  callback_url_allowlist:
    - api.example.com
```

Schema craft:

* **Closed enums on decision fields.** Your backend switches on `decision`; an enum makes drift impossible. (The runtime adds `additionalProperties: false` to every object node automatically.)
* **`required` everything you'll consume.** Optional output fields breed `KeyError`s downstream.
* **Validate inputs strictly.** A bad payload should be a `400` at the door (`input_validation_failed`), not a confused model mid-run.
* **Allowlist callbacks.** Without `callback_url_allowlist` any URL is accepted; with it, only the listed hostnames (a leading `.` matches subdomains).

## 2. Write the nodes

An autonomous routine is built from **TOOL**, **THINK**, and **routing-only** nodes — there are **no CHAT nodes**, because there's no customer in the conversation to speak to; the result leaves the run as typed JSON via `emit_output`, not as a message. (Node-type details: [Autonomous routines](/agents/concepts/autonomous-routines.md#node-types).) Two extra rules beyond the usual node craft:

1. The validated input is visible to every node (it arrives as a tool result in history) — reference its fields by name in instructions.
2. **Every terminal node (no outbound `transitions`) must be a TOOL node calling `built-in:emit_output`.** Terminal THINK nodes are rejected at validation time.

```yaml
id: kyc-decision
title: KYC Decision
conditions:
  - The KYC verification result for an applicant needs to be processed.
description: >
  Assess a completed KYC verification and produce an approve / reject /
  escalate decision with a one-sentence explanation.

entry: assess
nodes:
  - id: assess
    think: >
      Assess the verification payload from the input. GREEN with no
      failed checks means approved. RED means rejected. AMBER, or GREEN
      with any failed check, means escalate. Produce the decision and a
      one-sentence explanation referencing the decisive check.
    output_schema:
      type: object
      required: [decision, explanation]
      properties:
        decision:
          enum: [approved, rejected, escalate]
        explanation:
          type: string
    transitions:
      - to: finish

  - id: finish
    tools: built-in:emit_output
    tool_instruction: >
      Call emit_output with output_json set to a JSON object containing
      exactly the decision and explanation fields produced by the assess
      node.

autonomous:
  input_schema:
    type: object
    required: [applicant_id, verification_result]
    properties:
      applicant_id:
        type: string
      verification_result:
        type: object
        required: [status]
        properties:
          status:
            enum: [GREEN, AMBER, RED]
          checks:
            type: array
            items:
              type: object
  output_schema:
    type: object
    required: [decision, explanation]
    properties:
      decision:
        enum: [approved, rejected, escalate]
      explanation:
        type: string
  timeout_seconds: 60
  callback_url_allowlist:
    - api.example.com
```

Branched flows end every terminal node the same way — branch directly from the think node's transitions:

```yaml
  - id: assess
    think: >
      Assess the verification payload from the input. GREEN with no
      failed checks means approved. RED means rejected. AMBER, or GREEN
      with any failed check, means escalate.
    output_schema:
      type: object
      required: [decision, explanation]
      properties:
        decision:
          enum: [approved, rejected, escalate]
        explanation:
          type: string
    transitions:
      - to: emit-decision
        condition: "the decision is approved or rejected"
      - to: open-case
        condition: "the decision is escalate"

  - id: emit-decision
    tools: built-in:emit_output
    tool_instruction: >
      Call emit_output with output_json containing the decision and
      explanation from the assess node.

  - id: open-case
    tools: crm:create_review_case
    tool_instruction: >
      Create a manual-review case for applicant_id with the explanation
      as the case note.
    transitions:
      - to: emit-escalation

  - id: emit-escalation
    tools: built-in:emit_output
    tool_instruction: >
      Call emit_output with output_json containing decision "escalate"
      and the explanation from the assess node.
```

Budget the iteration cap: each tool/think node consumes an engine iteration, and `emit_output` is itself a tool call. A `think → tool → emit` flow needs 3; the default cap is 5 (`runtime.max_engine_iterations`). Runs that hit the cap before `emit_output` fail with `max_engine_iterations_reached`.

## 3. Reference it from the manifest

Autonomous routines are listed like any other:

```yaml
agent_config:
  context:
    routines:
      - id: kyc-decision
        version: 1
```

The `autonomous:` block in the routine YAML is what activates the trigger endpoint — there is no separate manifest switch. The boot log confirms registration per routine (timeout and webhook status included).

## 4. Trigger it

```bash
curl -sS -X POST "https://agent.example.com/routines/kyc-decision/trigger" \
  -H "Authorization: Bearer $AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "applicant_id": "app_123",
      "verification_result": {"status": "AMBER", "checks": [{"name": "selfie", "result": "WARN"}]}
    },
    "callback_url": "https://api.example.com/agent-callbacks",
    "idempotency_key": "kyc-app_123-attempt-1",
    "metadata": {"ticket": "OPS-441"}
  }'
```

Immediate `202`:

```json
{
  "run_id": "run_9f8e7d6c5b4a3f2e1d0c9b8a",
  "routine_id": "kyc-decision",
  "status": "accepted",
  "session_id": "sess_abc123",
  "created_at": "2026-06-04T10:15:00+00:00"
}
```

Always send an `idempotency_key` from a retry-capable caller — a duplicate key returns the existing run (HTTP 409) instead of running twice.

## 5. Receive the callback

Your endpoint gets one POST per run, authenticated with the agent's bearer key. Acknowledge fast; the server retries non-2xx responses with backoff (default 5 attempts).

```python
from fastapi import FastAPI, Request, Response

app = FastAPI()

AGENT_API_KEY = "devsecret"  # the same shared bearer token


@app.post("/agent-callbacks")
async def on_agent_callback(request: Request) -> Response:
    if request.headers.get("Authorization") != f"Bearer {AGENT_API_KEY}":
        return Response(status_code=401)

    payload = await request.json()

    if not mark_processed_once(payload["run_id"]):   # dedupe on run_id
        return Response(status_code=200)

    if payload["status"] == "succeeded":
        apply_kyc_decision(
            ticket=payload["metadata"]["ticket"],
            decision=payload["output"]["decision"],
            explanation=payload["output"]["explanation"],
        )
    else:
        alert_ops(
            ticket=payload["metadata"]["ticket"],
            code=payload["error"]["code"],
            message=payload["error"]["message"],
            trace_id=payload["trace_id"],
        )

    return Response(status_code=200)
```

Handle `status: "failed"` by `error.code` — the [failure taxonomy](/agents/concepts/autonomous-routines.md#failure-taxonomy) maps each code to the knob that fixes it. The full payload schema is in [Events & callbacks](/agents/reference/events-and-callbacks.md).

## 6. Optional: let the provider trigger it directly

Skip your own trigger plumbing by giving the routine a webhook:

```yaml
autonomous:
  # ... schemas as above ...
  webhook:
    secret_env: ${KYC_WEBHOOK_SECRET}
    header: X-Payload-Digest
    algorithm: sha256
    prefix: ""
```

Point the provider at `POST /webhooks/kyc-decision`, configured to sign the raw body with the shared secret. The provider's payload becomes the run's input (it must satisfy `input_schema`), provider retries dedupe automatically, and **no callback is sent** — webhook runs are fire-and-forget and always run in an ephemeral session; consume results from traces (set `traces.trace_id_field` so they're findable by your business key) or have a tool node before the terminal one write the outcome to your systems. For one URL dispatching to several routines, use manifest-level webhooks — see [Autonomous routines](/agents/concepts/autonomous-routines.md#webhook-entry-points).

To verify the wiring before pointing the real provider at it, sign a test body yourself:

```python
import hashlib
import hmac

secret = "the-shared-secret"
body = b'{"applicant_id": "app_123", "verification_result": {"status": "GREEN"}}'
signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
print(signature)
```

```bash
curl -sS -X POST "https://agent.example.com/webhooks/kyc-decision" \
  -H "X-Payload-Digest: <signature from above>" \
  -H "Content-Type: application/json" \
  --data-raw '{"applicant_id": "app_123", "verification_result": {"status": "GREEN"}}'
```

The body bytes must match the signed bytes exactly (the signature covers the raw body, not a re-serialisation). A `401` means signature mismatch — check algorithm, `prefix`, header name, and that both sides hold the same secret. Re-sending the same body returns the original run (HTTP 200) instead of firing twice.

## Testing checklist

* [ ] Happy path: trigger with a valid payload → callback `succeeded`, output matches `output_schema`
* [ ] Each branch reaches `emit_output` (trace shows the path taken)
* [ ] Invalid input → HTTP 400 at trigger time, no run started
* [ ] `timeout_seconds` is realistic: time the happy path, add headroom for model latency spikes
* [ ] Iteration budget: longest path's tool+think node count (+1 for `emit_output`) ≤ `max_engine_iterations`
* [ ] Callback receiver dedupes on `run_id` and returns 200 fast
* [ ] Retried trigger with the same `idempotency_key` does not double-run
* [ ] If webhook-enabled: provider signature verifies; replayed body returns the same run


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.interactive.ai/agents/guides/authoring-autonomous-routines.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
