# Aura Agent — YAML authoring guide (v1)

This guide is for **you** (or an LLM such as Claude/ChatGPT) editing a SIPSTACK
Aura agent exported as YAML. Edit the file, then re-import it in Switchboard
(**Aura AI → Agents → AI Voice Agents → Import**). An imported agent always lands
as an inert **draft** — it does not handle calls until you review it and click
**Activate**, so you can experiment safely.

Full reference (rendered): https://sipstack.com/docs/switchboard/aura/agent-yaml-authoring/

JSON Schema (for editor autocomplete / validation):
`https://sipstack.com/schemas/aura/agent.v1.json`
The exported file already points at it via a `# yaml-language-server: $schema=` line.

## Hard rules (an import is rejected if you break these)

1. **`auraAgentExportVersion: 1`** must be present.
2. `name`, `voiceName` and `greeting` are required (non-empty).
3. **Do not invent node ids.** Node ids are unique; every `edge.source` and
   `edge.target`, `flow.entryNodeId`, and every `eventHandlers` target must match
   an existing `node.id`.
4. `mode: flow` requires a `flow`.
5. **Do not write secret values.** Credentials appear as `${credential:Name}`
   placeholders — keep them as placeholders. You re-bind them after import.
6. If `flowMode: strict`, the flow may **not** use AI: no `say` node with
   `source.mode: llm`, no `intent-router`, `converse` or `kb-answer` nodes, and no
   `llmIntent` edge conditions.
7. Limits: `greeting` ≤ 4,000 chars, `systemPrompt` ≤ 32,000, `globalPrompt` ≤ 8,000;
   flow ≤ 500 nodes and ≤ 256 KB; ≤ 10 `postCallActions`; `silenceTimeoutMs`
   500–15,000; `maxCallDurationMs` 10,000–3,600,000; `maxTurns` 1–100.

## Top-level fields

| Field | Meaning |
|-------|---------|
| `name`, `type` | Agent name; `type` is `overflow\|sales\|support\|general\|custom` (default `overflow`). |
| `voiceName` | Voice persona id. If your account doesn't have it, import flags it for re-selection. |
| `greeting` | First line spoken (freeform mode; in flow mode the first `say` node speaks instead). |
| `mode` | `freeform` or `flow`. Omitted ⇒ `flow` when a `flow` is present, else `freeform`. |
| `flowMode` | `flexible` (default) or `strict` (deterministic only) — routing discipline inside a flow. |
| `systemPrompt`, `globalPrompt` | Persona / instructions; always-applied scope & refusal text. |
| `responseValidator` | `{ enabled: true }` turns on the pre-speech response check. |
| `chimeEnabled`, `bargeInEnabled`, `silenceTimeoutMs`, `maxCallDurationMs` | Call-handling knobs. |
| `maxTurns` | Optional per-agent turn cap, whole number 1-100. Omit for the platform default. |
| `escalationMode` | Optional escalation behaviour: `transfer` (default) or `ticket`. |
| `variableOverrides` | e.g. `{ company_name: "Acme" }`. |
| `fallbackDestination` | `{ type, id?, number? }` — always listed for review on import. |
| `extractionPlan`, `postCallActions`, `recording` | Post-call data + delivery (`webhook\|slack\|teams\|email\|sms`) + recording config. |
| `flow` | The conversation graph (below). Omit it for a prompt-only "freeform" agent. |

## The flow graph

`flow` is `{ version: 2, entryNodeId, nodes[], edges[], limits?, eventHandlers? }`.
Branch routing is carried entirely by **edge conditions** — a node never holds its
own routing.

### Node types

- **`start`** — entry marker referenced by `entryNodeId`.
- **`say`** — `source: { mode: static, text }` (verbatim, templated) or
  `source: { mode: llm, prompt }` (AI-generated). `interruptible` optional.
- **`collect`** — capture one caller turn into `vars.<variable>`. Optional `prompt`.
  `inputMode: dtmf` captures keypad digits (`dtmf: { maxDigits, finishOnKey, interDigitTimeoutMs }`).
- **`verified-collect`** — read-back / spell / confirm capture of `vars.<variable>`
  (`fieldType`: `email|phone|code|first-name|last-name|full-name|text`).
- **`branch`** — pure routing; outgoing edges carry the conditions.
- **`intent-router`** — ONE AI call picks from `intents: [{ label, description? }]`
  (1–20; `none` is reserved and added automatically). Label → `vars.<resultVar>`
  (default `intent`) + `vars.<resultVar>__confidence`. Key edges on the label
  (`condition: { var: "vars.intent", op: eq, value: "<label>" }`) plus an
  `isDefault` edge. Optional `transcriptWindow`, `includeVars`,
  `confidenceThreshold`, `clarifyPrompt`, `maxClarifyRounds` (1–3).
- **`converse`** — bounded open sub-conversation (`systemPrompt`, `resultVar`,
  `goal?`, `maxTurns` 1–50, `maxDurationMs?`, `exitCondition?`).
- **`kb-answer`** — answer from the Knowledge Base (`query`, `resultVar`, `topK`,
  `tagFilter?`, `fallbackText?`).
- **`set-variable`** — `assignments: [{ target, value | fromVar }]`.
- **`tool-call`** / **`http-action`** — sync HTTP call; parsed JSON →
  `vars.<resultVar>`. Auth via `credentialRef: ${credential:Name}`.
- **`webhook`** — fire-and-forget event (`event`, `payload?`).
- **`action`** — deliver a message mid-call to `webhook|slack|teams|email|sms`
  (`target`, `body?`, optional `resultVar` for success/failure branching).
- **`escalate-ticket`** — open a support ticket (`emailVar`, `request`, …).
- **`calendar-book`** — offer and book an appointment (`meetingType`, `resultVar`, …).
- **`transfer`** — hand back to the dialplan at `target`.
- **`hangup`** — end the call.

### Edges & conditions

```yaml
edges:
  - { id: e1, source: start,   target: greeting }
  - { id: e2, source: greeting, target: menu }
  - id: e3            # conditional edge off a branch
    source: menu
    target: sales
    condition: { var: "vars.intent", op: contains, value: "sales" }
  - { id: e4, source: menu, target: support, isDefault: true }   # fall-through
```

- **Predicate:** `{ var, op, value? }`. `op` ∈ `eq, neq, contains, regex, gt, lt,
  gte, lte, exists, notExists` (`value` omitted for `exists`/`notExists`).
- **AI intent:** `{ llmIntent: { prompt, expect } }` (not allowed in `strict` mode).
- **Combinators:** `{ all: [...] }`, `{ any: [...] }`, `{ not: {...} }`.
- **`isDefault: true`** marks the fall-through edge when no conditional matches.

Optional `flow.eventHandlers`: `globalEscape: [{ phrases, target }]`,
`noInput: { target }`, `noMatch: { maxRetries, target }`.
`globalEscape` is checked first on every utterance; `noInput` / `noMatch` fire
after a `collect` captures nothing (`noMatch` once `maxRetries` empty captures
in a row are reached, otherwise `noInput`).

Reference call context in templates with `{{...}}`: `vars.<name>` (collected turns +
tool results), bare shortcuts like `company_name`, `caller_number`, `current_date`,
and `caller.number`, `caller.name`, `caller.dialedNumber`, `agent.name`.
After a `tool-call`/`action` with a `resultVar`, failures surface as
`vars.<resultVar>__error` so an edge can branch on them.

## After you import

The agent is a **draft**. Switchboard shows a validation report listing anything that
needs your attention — a voice your account doesn't have, each `${credential:Name}`
to bind (create a Connection under **Aura AI → Agents → Connections** with exactly
that name), and the fallback destination. Resolve those, then **Activate**.
Recording is imported switched off — re-attest consent in the draft to enable it.
Point an inbound route at the agent (and test it in the **Test agent** tab) before
it takes live calls.
