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

# Check a playbook for problems before it goes live

> Statically check the playbook's saved graph and report what would go wrong on a real call. It answers the questions a canvas cannot: can every route actually reach the end, can a caller get stuck on a question, and — the important one — could an action step fire with a value that nothing in the conversation ever filled in. Nothing is changed or executed: this reads the saved graph, so it is safe to call as often as you like. It requires only the read scope for that reason. Findings come back in two lists. `blockers` are defects that will break a call (no start step; a question with no way onward; an action step whose argument nothing can ever produce, which would send that field empty). `warnings` are worth reading but do not stop anything (a step nothing connects to; a value bound on some routes and not others; spoken text naming a variable the playbook never produces, which the caller would hear read out literally). `graphStats` summarises the shape of what was checked. The same checks run automatically as part of getPrePublishReport, so a blocker here also appears there. Run this after editing a graph with putPlaybookGraph and fix the blockers before publishing.




## OpenAPI

````yaml /openapi.yaml post /agents/{agentId}/playbooks/{id}/verify
openapi: 3.1.0
info:
  title: Flowyte V2 Control-Plane REST API
  version: 1.0.0
  description: >-
    The single REST API for the Flowyte platform (base path `/api/v1`).
    Authenticate every request with a secret API key — `Authorization: Bearer
    flowyte_sk_…` — and each operation lists the scope the key must hold.
    Successful responses use the `ApiResponse<T>` envelope; list responses use
    cursor-based `PaginatedResponse<T>`. Errors are RFC 9457 problem+json.
    Streaming endpoints return Server-Sent Events
    (`event:<type>\ndata:<json>\n\n`, terminating with `event: done`).
  contact:
    name: Flowyte Platform
  license:
    name: Proprietary
servers:
  - url: /api/v1
    description: Flowyte control-plane (versioned URI; additive in v1).
security:
  - apiKey: []
tags:
  - name: Agents
    description: The single user-facing entity.
  - name: Support
    description: In-app "Get help" form → support inbox email.
  - name: Skills
    description: >
      Agent capabilities / tools. A skill is ONE atomic action — typically a
      single API call the agent makes in one step (look up an order, create a
      record, send a message, transfer the call). Use a skill when the task is a
      single step. When a task needs several details gathered across turns
      BEFORE acting, build a Playbook (which gathers the inputs and then calls
      skills) — see the Playbooks tag.
  - name: Integrations
    description: Native OAuth integrations.
  - name: Knowledge
    description: RAG knowledge sources & preview.
  - name: Playbooks
    description: >
      Multi-turn conversation scripts — a node graph (gather → confirm → branch)
      the agent follows to collect several inputs IN ORDER across turns before
      acting. Build a playbook when a single skill call isn't enough because the
      agent must gather MANY details first, or run a SEQUENCE of skills, before
      it can finish (e.g. take a full service request: gather the problem,
      address, and time, confirm, THEN file it; qualify a lead; a multi-step
      intake). A playbook does NOT call an integration itself — it owns the
      conversation and holds the state across turns; the actual action is
      performed by the SKILL(s) it gathers the inputs for. So a playbook
      ORCHESTRATES skills. Rule of thumb — one API call → a Skill; "gather N
      things in order, then submit" → a Playbook that drives the conversation
      and calls the skill(s) at the end.
  - name: Variables
    description: >
      The agent-wide interaction-variable registry — DERIVED at read time from
      the agent's playbooks and skills (collect slots, skill output bindings,
      {var} placeholders) and merged with a thin annotation overlay (notes,
      declared type hints, manual declarations). Read-only observation, NEVER a
      gate: it never validates a reference and never affects authoring, publish,
      or runtime behaviour.
  - name: Guardrails
    description: Deterministic guardrail policies & caller verification.
  - name: Numbers
    description: Phone numbers / DIDs.
  - name: SMS
    description: >
      SMS Hub — A2P 10DLC self-serve registration (brand + campaign), the free
      AI compliance review, per-number SMS enablement, and the consent/opt-out
      surface (suppressions, contacts, TCPA proof export). Texting is US-only; a
      number must be SMS-enabled and the org's 10DLC registration active before
      it can send.
  - name: Test
    description: Test, simulate, talk-token, probe.
  - name: Observe
    description: Post-call analytics, conversations, receipts, transcripts.
  - name: Billing
    description: Plans, wallet, usage, fixed phrases.
  - name: Voices
    description: Voice catalog.
  - name: ApiKeys
    description: Developer / API keys.
  - name: Webhooks
    description: Webhook endpoints & deliveries.
  - name: Records
    description: >
      Caller Context Store — pre-synced external records the agent greets a
      caller from, plus the learned object-type field registry. Fed by the
      Zapier app's Create/Update Record action and directly by this REST API;
      read at call time by the context_lookup skill (a sub-100ms local lookup,
      no Zapier round-trip).
  - name: AuditLogs
    description: API/key activity logs.
  - name: Chat
    description: Chat channel — sessions, messages, OpenAI-compatible, widget.
  - name: PublishableKeys
    description: Browser-safe, one-agent publishable keys.
  - name: Widget
    description: Embed widget config + snippet.
  - name: Uploads
    description: Multipart file uploads backing file_id params
  - name: Meta
    description: Platform metadata (language/SKU/tier capabilities).
  - name: Outbound
    description: >
      Outbound voice — contact lists (import + scrub) and campaigns (create,
      launch, pause/resume/cancel). Launch schedules one call attempt per valid
      contact; a background worker dials them. This release dials informational
      campaigns only.
  - name: Team
    description: >
      Team Members — the general "invite a teammate" surface (roster, the
      platform invitations, role changes/removals, and the domain-discovery
      join-request inbox). Distinct from the Flowyte Phone SEAT surface: a team
      invite mints NO softphone seat. Reads → team:read; mutations → team:write
      (owner/admin; staff is role-gated out). Owner-only guards protect owner
      grants/edits and the org's last owner; no self-role-change or
      self-removal.
  - name: Onboarding
    description: >
      Pre-org domain discovery — the signup-flow seam a just-signed-in user hits
      BEFORE they have a workspace, to learn "your coworkers may already be
      here" and ask to join. Engine-root, authenticated by a the platform
      session JWT that may have no active org; tenancy is derived SERVER-SIDE
      from the caller's verified email (anti-enumeration), never from input.
  - name: OAuth2
    description: >
      Flowyte's minimal OAuth2 authorization server. The Flowyte Zapier app is
      the first registered client. The authorization-code grant (confidential
      client, optional PKCE) issues org-scoped SERVICE tokens (flowyte_oat_…)
      that ride the SAME scope matrix as an sk key. The public
      authorize/token/revoke endpoints are served at the ORIGIN ROOT (NOT under
      /api/v1 — see each path's `servers` override); consent + disconnect are
      /api/v1.
paths:
  /agents/{agentId}/playbooks/{id}/verify:
    parameters:
      - $ref: '#/components/parameters/AgentIdPathScoped'
      - name: id
        in: path
        required: true
        description: >-
          The playbook's unique ID (the value returned by createPlaybook /
          listPlaybooks).
        example: pbk_9a2f1c7b04e34d02
        schema:
          type: string
    post:
      tags:
        - Playbooks
      summary: Check a playbook for problems before it goes live
      description: >
        Statically check the playbook's saved graph and report what would go
        wrong on a real call. It answers the questions a canvas cannot: can
        every route actually reach the end, can a caller get stuck on a
        question, and — the important one — could an action step fire with a
        value that nothing in the conversation ever filled in. Nothing is
        changed or executed: this reads the saved graph, so it is safe to call
        as often as you like. It requires only the read scope for that reason.
        Findings come back in two lists. `blockers` are defects that will break
        a call (no start step; a question with no way onward; an action step
        whose argument nothing can ever produce, which would send that field
        empty). `warnings` are worth reading but do not stop anything (a step
        nothing connects to; a value bound on some routes and not others; spoken
        text naming a variable the playbook never produces, which the caller
        would hear read out literally). `graphStats` summarises the shape of
        what was checked. The same checks run automatically as part of
        getPrePublishReport, so a blocker here also appears there. Run this
        after editing a graph with putPlaybookGraph and fix the blockers before
        publishing.
      operationId: verifyPlaybook
      responses:
        '200':
          description: >-
            The verification report. A 200 with a non-empty `blockers` list is
            the normal way a broken playbook is reported — it is not an error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PlaybookVerifyReport'
              example:
                success: true
                data:
                  blockers:
                    - rule: action_arg_unfillable
                      severity: blocker
                      nodeId: n_submit
                      nodeType: skill
                      slot: lead_name
                      message: >-
                        Step "n_submit" sends {lead_name}, but nothing before it
                        ever produces lead_name. That field would be sent empty
                        — collect it first, or bind it from an earlier step's
                        result.
                  warnings:
                    - rule: unreachable_step
                      severity: warning
                      nodeId: n_old_confirm
                      nodeType: message
                      message: >-
                        Step "n_old_confirm" (message) cannot be reached from
                        the start step, so it never runs. Connect it or remove
                        it.
                  graphStats:
                    steps: 15
                    connections: 25
                    collectSteps: 9
                    actionSteps: 2
                    messageSteps: 2
                    branchSteps: 0
                    reachableSteps: 15
                    slots:
                      - lead_address
                      - lead_bathrooms
                      - lead_bedrooms
                      - lead_dates
                      - lead_frequency
                      - lead_name
                      - lead_notes
                      - lead_service_type
                      - lead_zip
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - apiKey:
            - playbooks:read
components:
  parameters:
    AgentIdPathScoped:
      name: agentId
      in: path
      required: true
      description: >-
        The agent this resource is scoped to (the UUID returned by createAgent /
        listAgents).
      example: agt_4f9c2b7e10a24d51
      schema:
        type: string
  schemas:
    ApiResponse_PlaybookVerifyReport:
      allOf:
        - $ref: '#/components/schemas/ApiResponseBase'
        - type: object
          required:
            - data
          properties:
            data:
              $ref: '#/components/schemas/PlaybookVerifyReport'
    ApiResponseBase:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
        message:
          type: string
        errors:
          type: array
          items:
            type: object
            required:
              - field
              - message
            properties:
              field:
                type: string
              message:
                type: string
    PlaybookVerifyReport:
      type: object
      description: >-
        The result of verifyPlaybook. `blockers` empty means the graph passes
        every check the platform can make statically — it does not promise the
        wording is right, only that no route is broken and no action step can
        fire on a value nothing produces.
      required:
        - blockers
        - warnings
        - graphStats
      properties:
        blockers:
          type: array
          description: >-
            Defects that will break a real call. Fix these before publishing;
            they also appear in getPrePublishReport.
          items:
            $ref: '#/components/schemas/PlaybookVerifyFinding'
        warnings:
          type: array
          description: Worth reading, never a gate.
          items:
            $ref: '#/components/schemas/PlaybookVerifyFinding'
        graphStats:
          $ref: '#/components/schemas/PlaybookGraphStats'
    ProblemDetails:
      description: RFC 9457 problem+json.
      type: object
      properties:
        type:
          type: string
          format: uri
          default: about:blank
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        code:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
    PlaybookVerifyFinding:
      type: object
      description: >-
        One problem found in a playbook graph. `message` is a complete,
        plain-language sentence written for the person who built the playbook —
        show it as-is. `rule` is a stable machine key you can branch on;
        `nodeId`/`slot` anchor the finding to a step and a value when the
        problem is specific to one.
      required:
        - rule
        - severity
        - message
      properties:
        rule:
          type: string
          description: >-
            Stable identifier for the check that fired. Blockers:
            `no_start_step`, `multiple_start_steps`, `duplicate_step_id`,
            `dangling_edge`, `collect_step_has_no_exit`,
            `action_step_missing_tool`, `action_arg_unfillable`,
            `channel_mask_makes_slot_unfillable`. Warnings: `no_end_step`,
            `unreachable_step`, `action_arg_may_be_unbound`,
            `spoken_variable_unfillable`, `branch_variable_unfillable`,
            `action_tool_unknown`, `channel_mask_has_no_effect`,
            `channel_text_never_used`. `channel_mask_makes_slot_unfillable` is
            the one worth knowing by name, and its SEVERITY depends on the
            receiving skill: a step was switched off for a channel while an
            action step downstream still references the value it gathers. When
            the affected argument is REQUIRED by the target skill, that call
            cannot succeed on the channel — a blocker. When it is optional, the
            runtime simply OMITS the argument from the call (an unresolved
            argument is never sent as an empty string or a literal placeholder),
            which is often the intent — a warning, cleared by stating the intent
            with `args_by_channel` (below). `args_by_channel` (accepted as
            `argsByChannel`) on an action step scopes single arguments per
            channel the same way `prompt_by_channel` scopes text: an object of
            `{channel|default: {argName: template|null}}` overlaid onto `args`
            for the conversation's channel, where an explicit `null` REMOVES the
            argument on that channel — and fully silences this rule for it. This
            is how "ask for text-message consent on voice, and do not send the
            consent field from a text conversation" is expressed with one submit
            step instead of a duplicated write. `channel_text_never_used` covers
            the TWO ways a per-channel setting dies unread, and the `message`
            tells you which: the agent does not run on that channel at all (turn
            the channel on, or remove the setting), or the agent does but THIS
            STEP is switched off for it by `channels`/`skip_on` (let the step
            run there, or remove the wording). Both are relative to the channels
            the agent actually runs on, which is resolved identically here and
            in `GET /agents/{id}/prepublish-report` — the two reports cannot
            disagree about whether a channel is live.
        severity:
          type: string
          enum:
            - blocker
            - warning
          description: >-
            blocker = a real call would break; warning = worth reading, nothing
            is stopped.
        message:
          type: string
          description: The finding written out for a human. Safe to show verbatim.
        nodeId:
          type: string
          description: >-
            The step the finding is about. Absent when the finding is about the
            playbook as a whole.
        nodeType:
          type: string
          description: The step's type (collect, skill, message, branch, start, end).
        slot:
          type: string
          description: The value the finding is about, when there is one.
    PlaybookGraphStats:
      type: object
      description: >-
        The shape of the graph that was checked — a quick confirmation you
        verified what you meant to.
      required:
        - steps
        - connections
        - collectSteps
        - actionSteps
        - messageSteps
        - branchSteps
        - reachableSteps
        - slots
      properties:
        steps:
          type: integer
        connections:
          type: integer
        collectSteps:
          type: integer
          description: Steps that ask the caller for a value.
        actionSteps:
          type: integer
          description: Steps that call one of the agent's skills.
        messageSteps:
          type: integer
        branchSteps:
          type: integer
        reachableSteps:
          type: integer
          description: >-
            How many steps can actually be reached from the start step. Lower
            than `steps` means some are orphaned.
        slots:
          type: array
          description: Every value the playbook collects, sorted.
          items:
            type: string
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      headers:
        WWW-Authenticate:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    NotFound:
      description: Resource not found in org scope.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
  securitySchemes:
    apiKey:
      type: oauth2
      description: >
        Flowyte secret API key (`Authorization: Bearer flowyte_sk_live_…`).
        Scope-gated; is scoped to your organization — a key can never reach
        another tenant. The listed scopes in each operation's `apiKey`
        requirement are the scopes that key must hold. The `tokenUrl` is
        nominal: keys are minted in the dashboard.
      flows:
        clientCredentials:
          tokenUrl: /api/v1/api-keys
          scopes:
            agents:read: Read agents.
            agents:write: Create/update/delete agents, publish, rollback.
            knowledge:read: Read knowledge sources & preview.
            knowledge:write: Add/remove knowledge sources, uploads.
            skills:read: Read skills & skill-types.
            skills:write: Create/update/delete skills.
            playbooks:read: Read playbooks & graphs.
            playbooks:write: Create/update/delete playbooks & graphs.
            guardrails:read: Read guardrail policies & caller-verification.
            guardrails:write: Update guardrail policies & caller-verification.
            numbers:read: Read phone numbers / search availability.
            numbers:write: Purchase / assign / release numbers.
            sms:read: Read the org's SMS (10DLC) registration status and numbers.
            sms:write: Save/submit the 10DLC registration and toggle numbers for SMS.
            outbound:read: Read outbound contact lists and campaigns.
            outbound:write: >-
              Create/import contact lists, create/launch/pause/resume/cancel
              outbound campaigns, and enqueue single outbound calls.
            integrations:read: Read connected native integrations (status only — never tokens).
            integrations:write: Discover schemas, set data scoping, and disconnect a connection.
            integrations:connect: >-
              Connect a data source (submit credentials / begin OAuth) — a
              SEPARATE, higher-privilege scope because connecting INGESTS
              credentials and opens a new egress path; a discover/scope/author
              key need not carry it.
            records:read: >-
              Read the pre-synced Caller Context Store records and object-type
              field registry.
            records:write: >-
              Upsert / delete / bulk-import / purge caller-context records and
              edit type metadata.
            calls:read: Read conversations, receipts, transcripts, analytics.
            analytics:read: >-
              Read the Observe history list, per-agent analytics (the
              answer-rate summary), the raw knowledge-gap list, and the metric
              catalog + per-metric queries + metric drill-downs.
            analytics:write: >-
              Curate knowledge gaps (dismiss / mark in-progress). [M4 —
              reserved]
            dashboards:read: List and read saved Observe reporting dashboards.
            dashboards:write: >-
              Create, update (full-document replace), and delete Observe
              reporting dashboards.
            reports:read: List and read Observe scheduled reports (report schedules).
            reports:write: >-
              Create, update, and delete Observe scheduled reports (a delete
              stops a recurring digest).
            billing:read: Read plans, wallet, usage.
            audit:read: Read API/key activity logs.
            webhooks:write: Manage webhook endpoints.
            keys:write: Manage secret API keys.
            chat:read: Read chat sessions & messages.
            chat:write: Create chat sessions & send messages (server-side).
            widgets:read: Read widget config & embed snippet.
            widgets:write: Update widget config.
            pubkeys:read: Read publishable keys.
            pubkeys:write: Manage publishable keys.
            phone:read: >-
              Read Flowyte Phone config — settings, ring targets/queues +
              rosters, contacts, block-list, own presence.
            phone:write: >-
              Update Flowyte Phone config — settings, ring targets/queues +
              members, contacts, block-list.
            presence:write: Set your own softphone presence (available/away/dnd/…).
            team:read: Read the team — softphone seats and the team presence roster.
            team:write: >-
              Manage softphone seats — invite, update, and deactivate team
              members.
            escalation_destinations:read: Read external-agent (AI Harness) escalation destinations.
            escalation_destinations:write: >-
              Create, update, delete, and rotate the signing secret of
              escalation destinations.
            escalation_policies:read: Read an agent's escalation routing policy. [ — reserved]
            escalation_policies:write: Update an agent's escalation routing policy. [ — reserved]
            escalations:read: >-
              Read escalation sessions and their message history (connector). [
              — reserved]
            escalations:claim: >-
              Claim an escalation session and extend its lease (connector). [ —
              reserved]
            escalations:respond: >-
              Post messages into a claimed escalation session (connector). [ —
              reserved]
            escalations:resolve: >-
              Resolve, return, or request human takeover of an escalation
              (connector). [ — reserved]
            escalations:initiate: >-
              Start a NEW outbound SMS conversation with a customer (connector).
              Deliberately separate from escalations:respond — the other scopes
              work a conversation the platform handed over; this one texts a
              person who never contacted you on that thread. Also requires the
              org to be enabled for harness-initiated conversations.

````