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

# Test a Flow

> Walks a flow graph against a synthetic transcript without dispatching real tools or writing a `call_logs` row. Conversation-node transitions are picked by a deterministic keyword heuristic — for true eval-LLM-driven simulation use the in-app test panel. Tool-call nodes consume mocked results from the request body. Useful for CI smoke tests and skill verification before going live.


Hermetic flow simulator. Walks a `flow_config` graph against a synthetic transcript without dispatching real tools, writing a `call_logs` row, or placing a real call. Useful for CI smoke tests, skill verification, and pre-deploy sanity checks.

Required scope: `flows:test` (separate from `agents:update` because flow tests can spend money on eval LLMs and external APIs in richer test modes).

## What this endpoint does

* Loads the agent's saved `flow_config` (or uses the override you supply in the request body).
* For each **conversation node**: consumes the next `role: "user"` turn from your transcript and picks a transition by deterministic keyword overlap with each transition's label/description. Misses route to "stay".
* For each **tool-call node**: looks up `mock_tool_results[step_id]`. If `error` is set, takes the `error` transition. Otherwise takes `success`, with custom branches evaluated in declaration order via the `jsonpath`/`equals` rule.
* At an **end** node (or transfer, or post-end webhook/structured\_output), the walk terminates and returns the full trace.

## What this endpoint does NOT do

* It **does not** call the eval LLM. The deterministic heuristic is good enough for unit tests of branching topology, not for testing prompt quality. For eval-LLM-driven simulation use the in-app Flow Test panel (which runs a real bot pipeline against a WebRTC web call).
* It **does not** dispatch tools. Mock every tool-call node you reach via `mock_tool_results`.
* It **does not** write `call_logs` or fire webhooks.

## Body

```json theme={null}
{
  "transcript": [
    { "role": "user", "text": "I want to book for next Wednesday at 2pm" }
  ],
  "mock_tool_results": {
    "check-availability-step": {
      "result": { "status": "available", "slot_id": "abc123" }
    }
  }
}
```

Optionally include `flow_config` in the body to test an unsaved draft instead of the agent's stored graph.

## Response

```json theme={null}
{
  "trace": [
    { "step_id": "start-1", "kind": "enter" },
    { "step_id": "start-1", "kind": "auto_advance", "decision": "ask-date-1" },
    { "step_id": "ask-date-1", "kind": "enter" },
    { "step_id": "ask-date-1", "kind": "eval", "decision": "tx-confirm", "reason": "heuristic match (overlap_score=2)" },
    { "step_id": "check-availability-step", "kind": "enter" },
    { "step_id": "check-availability-step", "kind": "tool_mock", "decision": "confirm-step", "reason": "success", "data": { "status": "available", "slot_id": "abc123" } },
    { "step_id": "end-1", "kind": "enter" },
    { "step_id": "end-1", "kind": "end" }
  ],
  "named_results": { "Check Availability": { "status": "available", "slot_id": "abc123" } },
  "slot_values": {},
  "ended_at_step_id": "end-1"
}
```


## OpenAPI

````yaml POST /agents/{id}/flow/test
openapi: 3.1.0
info:
  title: Yappr API
  description: >
    Create and manage AI voice agents, purchase phone numbers, configure tools,
    and initiate calls — all via REST.
  version: 1.0.0
  contact:
    url: https://goyappr.com
servers:
  - url: https://api.goyappr.com
    description: Production
security:
  - apiKey: []
paths:
  /agents/{id}/flow/test:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Flow Agents
      summary: Test a flow (hermetic simulator)
      description: >
        Walks a flow graph against a synthetic transcript without dispatching
        real tools or writing a `call_logs` row. Conversation-node transitions
        are picked by a deterministic keyword heuristic — for true
        eval-LLM-driven simulation use the in-app test panel. Tool-call nodes
        consume mocked results from the request body. Useful for CI smoke tests
        and skill verification before going live.
      operationId: testAgentFlow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - transcript
              properties:
                flow_config:
                  $ref: '#/components/schemas/FlowConfig'
                  description: >-
                    Optional override. If omitted, uses the agent's saved
                    flow_config.
                transcript:
                  type: array
                  items:
                    type: object
                    required:
                      - role
                      - text
                    properties:
                      role:
                        type: string
                        enum:
                          - user
                          - agent
                      text:
                        type: string
                mock_tool_results:
                  type: object
                  additionalProperties:
                    type: object
                    properties:
                      result:
                        description: Whatever the tool would have returned.
                        additionalProperties: true
                      error:
                        type: string
                        description: >-
                          Set to mark the tool as having failed; routes through
                          the error transition.
                  description: Keyed by tool-call node id.
      responses:
        '200':
          description: Trace of nodes visited and decisions made
          content:
            application/json:
              schema:
                type: object
                properties:
                  trace:
                    type: array
                    items:
                      $ref: '#/components/schemas/FlowTraceEntry'
                  named_results:
                    type: object
                    additionalProperties: true
                  slot_values:
                    type: object
                    additionalProperties: true
                  ended_at_step_id:
                    type: string
                    nullable: true
        '400':
          description: Invalid flow_config, transcript, or missing mock results
        '404':
          description: Agent not found
components:
  schemas:
    FlowConfig:
      type: object
      required:
        - nodes
      description: >
        The full flow definition stored in `agents.flow_config`. Constraints
        enforced at validate-time

        (any failure returns 400 `FLOW_INVALID` with one issue per problem — see
        `FlowInvalidError`):


        - Exactly one node of `type=start` (`no_start` / `multiple_starts`).

        - Start node has a `next_step_id` (`start_unwired`).

        - All `next_step_id` references resolve to a real node id
        (`unknown_target_node`).

        - Node ids are unique.

        - Every conversation node has non-empty `instructions`
        (`instructions_missing`).

        - Every conversation transition has a non-empty `description`
        (`transition_description_missing`).
          The description is the natural-language trigger the voice agent reads at runtime to pick a path —
          a bare `label` is not sufficient.
        - Every tool_call node has a `tool_id` (`tool_id_missing`).

        - At most 127 unique typed extraction contracts may be exposed in one
        flow
          (`too_many_extraction_contracts`). Nodes with identical effective tool/config schemas
          deduplicate; integration arguments participate only in `ai_extract` mode.
        - Every integration_call node has a valid `action` for its `provider`
        (`action_invalid`),
          an `integration_id` (`integration_id_missing`), and that integration belongs to the caller's
          company and is `active` (`integration_not_in_company`). Provider validity is enforced at
          schema parse time (`schema_invalid` if missing/unknown).
        - tool_call / integration_call nodes have a wired `success_next_step_id`
        (`success_not_wired`).

        - transfer nodes have a `transfer_to` (`transfer_to_missing`).

        - **Only `end` and `transfer` nodes may be terminal.** conversation,
        tool_call, and integration_call
          nodes must have at least one outgoing edge (a transition for conversation; `success` for the others).
          Violations: `terminal_not_allowed` per offending node, `no_terminal` if the flow has none at all.
        - Every node must be reachable from start (`unreachable_node`).
      properties:
        nodes:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/FlowNode'
        flow_config_version:
          type: string
          default: '1'
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Optional metadata surfaced to the eval LLM context (e.g. agent
            purpose, customer name conventions).
          properties:
            custom_metadata_keys:
              type: array
              items:
                type: string
              description: >
                User-declared keys that this flow's `args_template` token
                interpolation expects to

                find under the `{{metadata.<key>}}` namespace at dispatch time.
                Surfaced in the

                dashboard so callers know which keys to pass in `POST /calls
                body.metadata`.

                Keys not listed here still resolve at runtime if passed (and to
                empty string if

                not) — this list is purely a hint for the UI / API consumers.
    FlowTraceEntry:
      type: object
      properties:
        step_id:
          type: string
        kind:
          type: string
          enum:
            - enter
            - eval
            - tool_mock
            - auto_advance
            - end
        decision:
          type: string
          nullable: true
          description: Chosen transition id or 'stay'
        reason:
          type: string
        data:
          description: Tool result that was injected (when kind=tool_mock)
          additionalProperties: true
    FlowNode:
      description: Discriminated by `type`.
      oneOf:
        - $ref: '#/components/schemas/StartNode'
        - $ref: '#/components/schemas/ConversationNode'
        - $ref: '#/components/schemas/ToolCallNode'
        - $ref: '#/components/schemas/IntegrationCallNode'
        - $ref: '#/components/schemas/TransferNode'
        - $ref: '#/components/schemas/EndNode'
      discriminator:
        propertyName: type
        mapping:
          start:
            $ref: '#/components/schemas/StartNode'
          conversation:
            $ref: '#/components/schemas/ConversationNode'
          tool_call:
            $ref: '#/components/schemas/ToolCallNode'
          integration_call:
            $ref: '#/components/schemas/IntegrationCallNode'
          transfer:
            $ref: '#/components/schemas/TransferNode'
          end:
            $ref: '#/components/schemas/EndNode'
    StartNode:
      type: object
      required:
        - id
        - type
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - start
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        agent_speaks_first:
          type: boolean
          default: true
          description: >
            Whether the bot speaks first on call connect. When false, the bot
            waits silently for the caller to speak first (silence_timeout_secs
            on the agent still applies). For flow agents this OVERRIDES the
            agent-level agent.agent_speaks_first / agent.greeting_message
            fields.
        greeting:
          type: string
          nullable: true
          description: Spoken greeting (only used when agent_speaks_first is true)
        is_literal:
          type: boolean
          default: false
          description: >-
            When true, speak `greeting` verbatim. When false, treat it as an LLM
            instruction.
        next_step_id:
          type: string
          nullable: true
        auto_advance:
          type: boolean
          default: true
          description: >
            Whether to enter the first conversation node immediately on session
            start (default `true`). When `false`, the bot's greeting is
            delivered in start-node context only; the first conversation node is
            entered only after the user's first reply (via internal
            advancement).
    ConversationNode:
      type: object
      required:
        - id
        - type
        - instructions
        - transitions
      allOf:
        - $ref: '#/components/schemas/GlobalNodeFields'
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - conversation
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        instructions:
          type: string
          description: >-
            Layered on top of agent.system_prompt as a system message at step
            entry
        transitions:
          type: array
          items:
            $ref: '#/components/schemas/Transition'
    ToolCallNode:
      type: object
      required:
        - id
        - type
        - transitions
      description: >
        Deterministic tool dispatch. Tool args are owned by the tool itself

        (`payload_config.static_parameters` for literals plus

        `payload_config.extraction_parameters` for runtime-extracted values) —

        a `tool_call` node carries no per-node `args_template`. The same tool

        used by N flow nodes always sends the same shape; if you need a

        different shape per step, create a separate tool or supply a

        `config_override`. At call start, the effective referenced-tool config
        is

        registered as a flat argument-submission schema: one named string field

        per `extraction_parameters` entry, with `required` (default `true`)

        controlling which fields must be collected. The model never submits a

        `node_id` or nested `args` object. The runtime assembles standard call

        metadata and static parameters; the submitter exposes only extraction

        fields. Payload merge order is standard metadata, then static
        parameters,

        then extracted values, so extracted values win deliberate name

        collisions. Keep names unique unless that override is intentional.

        Schemas remain fixed for that live call; tool/config-override edits
        apply

        on the next call. Stale

        `args_template` payloads on this node type are silently dropped at parse

        time.
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - tool_call
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        tool_id:
          type: string
          format: uuid
          nullable: true
          description: References an existing row in the company's tools table
        config_override:
          type: object
          additionalProperties: true
          nullable: true
          description: >-
            Per-node override merged shallow over the referenced tool's config
            (array replacement). Open-shape — each tool type has its own valid
            keys.
        transitions:
          $ref: '#/components/schemas/ToolCallTransitions'
        pre_fire_announcement:
          type: boolean
          nullable: true
          description: >
            When true, the platform plays a short hold tone the moment this node
            fires so the caller

            doesn't sit in silence while the action runs. The tone is
            platform-controlled (not

            configurable) and stops automatically when the action returns. Use
            it for actions you

            expect to take more than ~500 ms (webhook to a slow CRM, complex DB
            lookup); skip it for

            fast actions to avoid making the call feel chatty.
        timeout_secs:
          type: number
          nullable: true
          minimum: 1
          maximum: 600
          description: >
            Per-node hard cap on execution time. When the action doesn't return
            in this many seconds,

            the runtime cancels it and routes to `error_next_step_id` with a
            `tool_timeout_after_Ns`

            error. When null/omitted on a webhook tool, the controller uses the
            effective tool

            `config.timeout_seconds` plus one second of dispatch overhead. Other
            tool nodes use the

            platform default (30s). Set this only when the flow needs a
            different node-level limit.
    IntegrationCallNode:
      type: object
      required:
        - id
        - type
        - provider
        - integration_id
        - action
        - transitions
      description: >
        Calls an OAuth-backed third-party integration (e.g. Google Calendar,
        Gmail) directly from the flow,

        without going through the `tools` table. The integration config —
        provider, account, action — lives on

        the node itself; the runtime resolves each entry in `args_template` per
        its declared mode (literal /

        ai_extract), interpolating `{{node.arg}}` and `{{metadata.key}}`
        mustache tokens, and dispatches

        against the matching integration client. Routing is deterministic and
        identical to `tool_call`

        (success / error / custom JSONPath branches, mutually exclusive, exactly
        one out-edge per fire).


        `provider` is locked at node creation; you cannot flip Calendar → Gmail
        on an existing node.

        `integration_id` must reference an `active` row in the caller's company
        `integrations` table whose

        `provider` matches this node's `provider`. `action` must be one of the
        actions in the catalog for the

        chosen provider.


        Action catalog:

        - `google_calendar.create_event` — required args: `summary`,
        `start_time`, `end_time`. Optional: `attendees`, `description`,
        `location`, `calendar_id`, `time_zone`.

        - `google_calendar.list_events` — optional args: `time_min`, `time_max`,
        `max_results`, `query`, `calendar_id`, `time_zone`.

        - `google_calendar.check_availability` — required args: `start_time`,
        `end_time`. Optional: `calendar_id`, `time_zone`.

        - `google_calendar.cancel_event` — required args: `event_id`. The
        runtime auto-resolves which calendar the event lives on (tries `primary`
        first, falls back to scanning the user's other writable calendars on a
        404), so `calendar_id` is intentionally NOT in the catalog for this
        action.

        - `gmail.send_email` — required args: `to`, `subject`, `body`. Optional:
        `html`, `cc`, `bcc`.


        For Google Calendar actions: `calendar_id` defaults to the user's
        `primary` calendar when blank. `time_zone` is an IANA name
        (`"Asia/Jerusalem"`) — when set, both Google's response is pinned to
        that zone and the event being created is stamped with it.


        **Calendar response post-processing**: `create_event`, `list_events`,
        and `check_availability` responses are sanitized for the LLM before they
        reach the agent — Gemini Live's ISO 8601 parser handles offsets
        unreliably, so the runtime strips the offset and seconds from each
        event's `dateTime` (leaving wall-clock format `"2026-05-10 16:30"`),
        removes per-event `timeZone` fields, and prepends a top-level `timeZone`
        + `timeZone_note` anchor. The agent only ever sees the sanitized view;
        JSONPath custom transitions (`transitions.custom[].jsonpath`) match
        against the sanitized view too. The raw, untouched Google response is
        preserved separately on the call event as `raw_response_preview` (see
        `FlowTraceToolCall` schema) for audit.
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - integration_call
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        provider:
          type: string
          enum:
            - google_calendar
            - gmail
          description: >-
            Locked at node creation. Must match the linked integration's
            provider.
        integration_id:
          type: string
          format: uuid
          description: >-
            References an active row in the caller's company integrations table
            whose provider matches this node's provider.
        action:
          type: string
          description: >-
            Provider-scoped action identifier — see the action catalog in the
            description.
          example: create_event
        args_template:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/ArgValue'
          description: >
            Map of arg name → `ArgValue`. Required keys depend on the chosen
            action (see catalog above).

            Each `ArgValue` is one of: literal string, `{mode:'literal'}`, or
            `{mode:'ai_extract'}`.

            Both `literal.value` and `ai_extract.description` may contain
            `{{<node_id>.<arg_name>}}`

            tokens (cross-node slot refs to earlier `ai_extract` args) and
            `{{metadata.<key>}}`

            tokens (per-call metadata, built-in keys plus any user-declared
            custom keys; see

            `CallMetadataKey` and `FlowConfig.metadata.custom_metadata_keys`).
        pre_fire_announcement:
          type: boolean
          nullable: true
          description: >
            When true, the platform plays a short hold tone the moment this node
            fires so the caller

            doesn't sit in silence while the action runs. Tone is
            platform-controlled (not

            configurable) and stops automatically when the action returns.
            Recommended for

            `create_event` / `send_email` / network-bound actions; skip for
            `check_availability`

            which is fast.
        timeout_secs:
          type: number
          nullable: true
          minimum: 1
          maximum: 600
          description: >
            Per-node hard cap on execution time. When the action doesn't return
            in this many seconds,

            the runtime cancels it and routes to `error_next_step_id`. Null =
            use the platform

            default (30s).
        transitions:
          $ref: '#/components/schemas/ToolCallTransitions'
    TransferNode:
      type: object
      required:
        - id
        - type
      allOf:
        - $ref: '#/components/schemas/GlobalNodeFields'
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - transfer
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        transfer_to:
          type: string
          nullable: true
          example: '+972501234567'
        transfer_message:
          type: string
          nullable: true
    EndNode:
      type: object
      required:
        - id
        - type
      allOf:
        - $ref: '#/components/schemas/GlobalNodeFields'
      description: >
        Terminal. Reaching an End node hangs up the call. For per-call data
        extraction or webhook delivery, use the agent-level
        `extraction_parameters` and `webhook_url` / `webhook_events` fields on
        the agent (those apply uniformly to prompt + flow agents).
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - end
        name:
          type: string
        position:
          $ref: '#/components/schemas/FlowNodePosition'
        farewell:
          type: string
          nullable: true
        is_literal:
          type: boolean
          default: false
    FlowNodePosition:
      type: object
      description: >-
        UI-only persisted x/y on the React Flow canvas. The runtime ignores
        this.
      properties:
        x:
          type: number
        'y':
          type: number
    GlobalNodeFields:
      type: object
      description: >
        Optional fields available on every node EXCEPT start and tool_call. When
        `is_global`

        is true, the node is reachable from any conversation node without an
        explicit edge —

        the eval LLM gets it as an additional candidate transition with a strong
        "prefer

        labeled transitions" bias. Use sparingly (recommended ≤3 globals per
        flow). Good for

        misclassification recovery and universal escape hatches; bad for
        happy-path shortcuts.
      properties:
        is_global:
          type: boolean
          default: false
          description: >-
            Mark this node as globally reachable. Only valid on
            conversation/end/transfer nodes.
        global_jump_description:
          type: string
          nullable: true
          description: >
            REQUIRED when is_global=true. Natural-language condition the eval
            LLM uses to decide whether

            to jump here. Describe a clear user-side signal, not an agent
            intent.

            Example: "User reveals they're actually an owner, not a tenant".
    Transition:
      type: object
      required:
        - id
        - label
        - description
        - next_step_id
      description: >
        A labeled outgoing branch from a conversation node. The voice agent
        picks one

        (or stays) at each user-turn boundary by calling the `pick_transition`
        tool.
      properties:
        id:
          type: string
          description: Stable transition id (used in the flow graph and traces)
        label:
          type: string
          description: >-
            Short human-readable label, also surfaced in the dashboard and call
            traces
        description:
          type: string
          minLength: 1
          description: >
            REQUIRED, non-empty. The natural-language trigger the voice agent
            reads at runtime

            to decide whether to take this path. Describe a user-side signal
            (what the caller

            said or implied), not an agent intent. Example: "Caller confirmed
            they want a demo

            scheduled" — not "agent books the demo". An empty description is
            rejected with

            error code `transition_description_missing`.
        next_step_id:
          type: string
          description: Destination node id
    ToolCallTransitions:
      type: object
      description: >
        Routing for a tool_call node. Routing is deterministic, no LLM. error
        fires on hard failures

        (timeout, 4xx/5xx, integration disconnected). Otherwise custom[] is
        evaluated in declaration order

        and the first match wins; success fires only if no custom matched.
        Exactly one out-edge per fire.
      properties:
        success_next_step_id:
          type: string
          nullable: true
        error_next_step_id:
          type: string
          nullable: true
        custom:
          type: array
          default: []
          items:
            $ref: '#/components/schemas/CustomTransition'
    ArgValue:
      description: >
        Discriminated union for one entry in
        `IntegrationCallNode.args_template`. Each arg can be one of

        three shapes:

          1. Bare string — shorthand for `{mode: 'literal', value: '<the string>'}`.
          2. `{mode: 'literal',    value}` — sent as-is.
          3. `{mode: 'ai_extract', description}` — the live agent runtime extracts this arg from the
             conversation right before the action fires; `description` guides extraction.

        Both `value` and `description` strings may contain mustache tokens that
        are interpolated at

        dispatch time:

          - `{{<node_id>.<arg_name>}}` — resolves to the value an earlier node AI-extracted from
            the conversation. Both `integration_call` AND `tool_call` source nodes are addressable.
            For `integration_call` sources the referenced arg must be declared in `ai_extract` mode
            in that node's `args_template` (validated at save). For `tool_call` sources the arg
            name must match an entry in the linked tool's `config.payload_config.extraction_parameters`
            — these aren't double-checked at save time because they live on the tool config (not the
            flow_config), so a typo renders to empty string at runtime; design an `error` branch
            on the downstream node. Refs to nodes that don't exist raise `args_template_dangling_reference`.
          - `{{metadata.<key>}}` — resolves against per-call metadata. Built-in keys: `id`,
            `direction`, `agent_number`, `user_number`, `agent_name`. User-defined keys come from
            the `metadata` dict passed to `POST /calls`; declare expected custom keys via
            `FlowConfig.metadata.custom_metadata_keys`. Missing metadata keys resolve to an empty
            string at runtime — NOT a save-time error.
      oneOf:
        - type: string
          description: >-
            Literal-mode shorthand. May contain `{{node.arg}}` /
            `{{metadata.key}}` tokens.
        - type: object
          required:
            - mode
            - value
          properties:
            mode:
              type: string
              enum:
                - literal
            value:
              type: string
              description: >-
                Literal value. May contain `{{node.arg}}` / `{{metadata.key}}`
                tokens.
        - type: object
          required:
            - mode
            - description
          properties:
            mode:
              type: string
              enum:
                - ai_extract
            description:
              type: string
              description: >-
                Natural-language hint the runtime uses to bind this slot from
                conversation. May contain `{{node.arg}}` / `{{metadata.key}}`
                tokens to splice prior context into the extraction prompt.
    CustomTransition:
      type: object
      required:
        - id
        - label
        - jsonpath
        - equals
        - next_step_id
      description: >
        Deterministic branch for a tool-call node, matched by JSONPath equality
        against the tool's parsed response body.

        Custom branches are evaluated top-to-bottom, first match wins; mutually
        exclusive with success/error (only one

        out-edge fires per tool fire). See
        /docs/api-reference/agents/flow-config-schema for the full runtime
        semantics.
      properties:
        id:
          type: string
        label:
          type: string
        jsonpath:
          type: string
          example: $.status
          description: >
            Dotted path into the tool's parsed response body (root `$`).
            Supported: `$.foo.bar`, `$.list[0].name`,

            `$.items[2]`. NOT supported: recursive descent (`$..`), wildcards
            (`$.*`), filter expressions.
        equals:
          type: string
          example: no_availability
          description: >
            Compared via string equality after JSON-style stringification of the
            extracted value:

            booleans become "true"/"false" (lowercase), null becomes "null",
            numbers via str(). Match the

            stringified form exactly or the branch never fires.
        next_step_id:
          type: string
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: >-
        Your Yappr API key (e.g. `ypr_live_...`). Generate one in the dashboard
        under **Settings → API Keys**.

````