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

> ## Agent Instructions
> Use the public API base URL: https://api.teamfollowup.ai/api.
> Authenticate public API requests with Authorization: Bearer YOUR_API_KEY.
> Use product-owned terms: agents, calls, campaigns, contacts, phone numbers, billing, and configuration.

# Replace the entire workflow

> **Only use this when you are authoring the whole graph.** For an ordinary edit reach for `PATCH .../workflow`, `POST .../outcomes/{outcome}/nodes` or `DELETE .../nodes/{nodeId}` — they carry no graph, so they cannot overwrite one.

Whole-graph upsert (create-or-update). Replaces `nodes`, `edges` and `dispositionEntries` in one write. The graph is your additions on top of the built-in per-agent-type base pass (auto outcome-tagging, power-dialer, alerts) that runs on every call regardless of what you save here. Structure is validated (node types, edges, branch handles, acyclicity); per-node config is not required so partial drafts are accepted, and anything incomplete comes back in `warnings`. Pass `expectedVersion` for optimistic concurrency.

**Omitting a field does not clear it.** `campaignConfig` and a node's coordinates are kept as stored when the request leaves them out, so a read-modify-write never silently discards settings you did not send. Send `"campaignConfig": null` to clear it on purpose.

**Omitting a node does not delete it.** A request that drops a node or an outcome entry that currently exists is rejected with 400, naming exactly what would be destroyed, unless you pass `"allowRemovals": true`.

**This replaces the whole graph, so prefer a narrower endpoint when one fits.** Whatever you send here overwrites `nodes`, `edges` and `dispositionEntries` outright, including any change someone else made after you read them. The narrower writes never carry a graph, so they cannot destroy one:
- add steps → `POST /outcomes/{outcome}/nodes` (also derives the node ids, edge ids and branch handles this endpoint expects you to invent correctly)
- change a step's config, a setting, or both together → `PATCH /workflow` (one write, so the pair cannot half-apply)
- remove one step and re-link the chain → `DELETE /nodes/{nodeId}`

Use this endpoint when you genuinely are authoring the whole graph, or for the things only it can do: saving an unconfigured draft node, repositioning, or editing an `if`/`switch` config.

**Concurrency.** Send `expectedVersion` and a write that lost the race is rejected with 409 instead of overwriting the winner — this is the only thing that protects you against your own read being stale, so send it. Without it, an API key is still guarded against the narrower window inside this request, but that will not save you from a graph that changed while you were editing.

Required API key scope: `campaign_workflows:write`.



## OpenAPI

````yaml /openapi.yaml put /api/agent-builder/agents/{id}/campaign/workflow
openapi: 3.1.0
info:
  title: TFUAI Server API
  version: 1.0.0
  description: >-
    API reference for the TFUAI SaaS server.


    All published endpoints are mounted under `/api`. Responses use a standard
    envelope: `{ "success": true, ... }` on success and `{ "success": false,
    "error": "..." }` on failure.


    Product API modules are being launched module by module. This spec currently
    exposes only contracted public modules.


    Authenticate public API requests with an API key in the `Authorization`
    header: `Authorization: Bearer YOUR_API_KEY`.
servers:
  - url: https://api.teamfollowup.ai
    description: Production API origin. Public API paths are under /api.
  - url: https://devapi.teamfollowup.ai
    description: Development API origin. Public API paths are under /api.
security: []
tags:
  - name: Agents
    description: Manage voice agents, campaigns, workflows, and outcomes.
  - name: Analytics
    description: >-
      Minimal, non-billing performance summary: volume, conversion, and pickup
      metrics.
  - name: Calls
    description: Review calls, test runs, and execution history.
  - name: Campaign Workflows
    description: >-
      What runs after a call lands: for each outcome (booked, opted out, asked
      for a callback…), an ordered chain of actions — tag the contact, move a
      pipeline stage, send an SMS, book an appointment. Stored as a graph:
      `nodes` are the actions, `edges` say what follows what, and
      `dispositionEntries` maps each outcome to the node its chain starts at.


      **Pick the narrowest endpoint that does the job.**


      | To | Call | |

      | --- | --- | --- |

      | See what fires per outcome | `GET .../workflow/digest` | Reads as plain
      text, no graph walking |

      | Add steps to an outcome | `POST .../outcomes/{outcome}/nodes` | Server
      derives node ids, edge ids, handles, layout |

      | Change a message, tag or setting | `PATCH .../workflow` | Merges;
      carries no graph, so it cannot damage one |

      | Remove one step | `DELETE .../nodes/{nodeId}` | Also re-links the chain
      around it |

      | Author or replace the whole graph | `PUT .../workflow` | Replaces
      everything you send |


      `PUT` is the only one that can re-wire, position nodes, build an `if`,
      save an unconfigured draft, or copy a whole workflow — and the only one
      that can overwrite a change someone else made after you read it. Use it
      when you are genuinely authoring the graph, and send `expectedVersion`
      when you do. For everything else the narrower endpoints are both easier
      and safer.
  - name: Contacts
    description: Contact list, filters, contact call history, and do-not-call actions.
  - name: GHL
    description: CRM variables and contact helpers used by agent and campaign setup flows.
  - name: Phone Numbers
    description: >-
      Caller ID pool, number search, assignment, movement, and release
      endpoints.
  - name: Power Dialer
    description: >-
      Power Dialer queue, lead cadence, callback, parked lead, and campaign
      schedule endpoints.
  - name: Projects
    description: >-
      Project management endpoints: list, update, disconnect, configure project
      campaigns, and validate setup.
  - name: Skills and Tools
    description: Manage reusable agent behaviors and in-call tool descriptions.
  - name: Voices
    description: Voice catalogue endpoints for choosing the voice used by an agent.
paths:
  /api/agent-builder/agents/{id}/campaign/workflow:
    put:
      tags:
        - Campaign Workflows
      summary: Replace the entire workflow
      description: >-
        **Only use this when you are authoring the whole graph.** For an
        ordinary edit reach for `PATCH .../workflow`, `POST
        .../outcomes/{outcome}/nodes` or `DELETE .../nodes/{nodeId}` — they
        carry no graph, so they cannot overwrite one.


        Whole-graph upsert (create-or-update). Replaces `nodes`, `edges` and
        `dispositionEntries` in one write. The graph is your additions on top of
        the built-in per-agent-type base pass (auto outcome-tagging,
        power-dialer, alerts) that runs on every call regardless of what you
        save here. Structure is validated (node types, edges, branch handles,
        acyclicity); per-node config is not required so partial drafts are
        accepted, and anything incomplete comes back in `warnings`. Pass
        `expectedVersion` for optimistic concurrency.


        **Omitting a field does not clear it.** `campaignConfig` and a node's
        coordinates are kept as stored when the request leaves them out, so a
        read-modify-write never silently discards settings you did not send.
        Send `"campaignConfig": null` to clear it on purpose.


        **Omitting a node does not delete it.** A request that drops a node or
        an outcome entry that currently exists is rejected with 400, naming
        exactly what would be destroyed, unless you pass `"allowRemovals":
        true`.


        **This replaces the whole graph, so prefer a narrower endpoint when one
        fits.** Whatever you send here overwrites `nodes`, `edges` and
        `dispositionEntries` outright, including any change someone else made
        after you read them. The narrower writes never carry a graph, so they
        cannot destroy one:

        - add steps → `POST /outcomes/{outcome}/nodes` (also derives the node
        ids, edge ids and branch handles this endpoint expects you to invent
        correctly)

        - change a step's config, a setting, or both together → `PATCH
        /workflow` (one write, so the pair cannot half-apply)

        - remove one step and re-link the chain → `DELETE /nodes/{nodeId}`


        Use this endpoint when you genuinely are authoring the whole graph, or
        for the things only it can do: saving an unconfigured draft node,
        repositioning, or editing an `if`/`switch` config.


        **Concurrency.** Send `expectedVersion` and a write that lost the race
        is rejected with 409 instead of overwriting the winner — this is the
        only thing that protects you against your own read being stale, so send
        it. Without it, an API key is still guarded against the narrower window
        inside this request, but that will not save you from a graph that
        changed while you were editing.


        Required API key scope: `campaign_workflows:write`.
      operationId: save-campaign-workflow
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            minLength: 1
          description: Agent id.
          example: agent_1ffdb9717444d0e77346838911
        - name: locationId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
          description: Pair with campaignId; omit both for primary campaign.
          example: loc_abc123
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
            minLength: 1
          description: Pair with locationId; omit both for primary campaign.
          example: cmp_123
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignWorkflowsUpsertRequest'
            examples:
              default:
                value:
                  nodes:
                    tag_booked:
                      id: tag_booked
                      type: add-tag
                      config:
                        tags:
                          - booked_via_ai
                    sms_reminder:
                      id: sms_reminder
                      type: send-sms
                      config:
                        message: Thanks for booking! See you soon.
                  edges:
                    - id: e_booked
                      source: tag_booked
                      target: sms_reminder
                  dispositionEntries:
                    SUCCESS: tag_booked
      responses:
        '200':
          description: The saved workflow.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignWorkflowsReadResponse'
              examples:
                default:
                  value:
                    success: true
                    workflow:
                      locationId: loc_abc123
                      campaignId: cmp_123
                      campaignConfig:
                        callbackMode: ai
                      nodes:
                        tag_booked:
                          id: tag_booked
                          type: add-tag
                          config:
                            tags:
                              - booked_via_ai
                        sms_reminder:
                          id: sms_reminder
                          type: send-sms
                          config:
                            message: Thanks for booking! See you soon.
                      edges:
                        - id: e_booked
                          source: tag_booked
                          target: sms_reminder
                      dispositionEntries:
                        SUCCESS: tag_booked
                      version: 4
                      createdAt: '2026-07-01T09:15:22.000Z'
                      updatedAt: '2026-07-14T16:04:11.000Z'
        '400':
          description: Validation failed or the request is malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignWorkflowsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: VALIDATION
                    message: Workflow graph must have a nodes object.
        '401':
          description: Missing or invalid authentication.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignWorkflowsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: Unauthorized
                    message: Authentication required.
        '403':
          description: Authenticated but not permitted to access this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignWorkflowsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: FORBIDDEN
                    message: You do not have access to this project.
        '404':
          description: Campaign or workflow not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignWorkflowsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: NOT_FOUND
                    message: Campaign not found on this project.
        '409':
          description: The workflow changed since it was loaded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignWorkflowsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: CONFLICT
                    message: >-
                      The workflow changed since it was loaded. Re-read and
                      retry.
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignWorkflowsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: RateLimited
                    message: Rate limit exceeded. Retry after a short delay.
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |-
            curl --request PUT \
              --url https://api.teamfollowup.ai/api/agent-builder/agents/{id}/campaign/workflow \
              --header 'Authorization: Bearer YOUR_API_KEY' \
              --header 'Content-Type: application/json' \
              --data '{
              "nodes": {
                "tag_booked": {
                  "id": "tag_booked",
                  "type": "add-tag",
                  "config": {
                    "tags": [
                      "booked_via_ai"
                    ]
                  }
                },
                "sms_reminder": {
                  "id": "sms_reminder",
                  "type": "send-sms",
                  "config": {
                    "message": "Thanks for booking! See you soon."
                  }
                }
              },
              "edges": [
                {
                  "id": "e_booked",
                  "source": "tag_booked",
                  "target": "sms_reminder"
                }
              ],
              "dispositionEntries": {
                "SUCCESS": "tag_booked"
              }
            }'
components:
  schemas:
    CampaignWorkflowsUpsertRequest:
      type: object
      additionalProperties: false
      required:
        - nodes
        - edges
        - dispositionEntries
      properties:
        nodes:
          type: object
          description: Workflow nodes keyed by node id.
          additionalProperties:
            $ref: '#/components/schemas/CampaignWorkflowsGraphNode'
        edges:
          type: array
          description: Directed connections between workflow nodes.
          items:
            $ref: '#/components/schemas/CampaignWorkflowsGraphEdge'
        dispositionEntries:
          type: object
          description: >-
            Maps each call outcome to the id of the node that starts its action
            chain. Keys are uppercase outcome constants: SUCCESS/BOOKED
            (booked), CONFIRMED (appointment confirmed), RESCHEDULE (rebooked),
            TRANSFER (live-transferred), TAG_APPLIED (tag objective met),
            CALLBACK (asked for a callback), INFO_ONLY/FOLLOWUP_REQUESTED (asked
            for information), HUMAN_NEEDED (asked for a person),
            VOICEMAIL/NO_ACTION (no outcome), KILL_SWITCH (opted out),
            FAILED_TRANSFER (transfer did not connect). Which outcomes an agent
            can emit depends on its type; the legal set per agent type is
            returned as `dispositions` by GET /api/v2/projects/systems. The
            value is the id of a node in `nodes` — the first node of your action
            chain for that outcome. The platform composes the canonical handler
            for each outcome automatically, so just attach your actions here; do
            not build your own trigger/detector nodes to re-detect a standard
            outcome. 


            `INFO_ONLY` and `FOLLOWUP_REQUESTED` are two spellings of the same
            outcome ("asked for information") and are treated as interchangeable
            at run time: outbound agent types emit `INFO_ONLY` and the inbound
            receptionist emits `FOLLOWUP_REQUESTED`, and a chain wired under
            either spelling fires for both. Use whichever the systems endpoint
            lists for your agent type. 


            A key no agent type can emit is accepted but never fires — the
            response reports it as an `unknown_outcome` warning rather than
            rejecting the write, because graphs predating a rename rely on being
            readable.
          additionalProperties:
            type: string
        campaignConfig:
          $ref: '#/components/schemas/CampaignWorkflowsCampaignConfig'
        expectedVersion:
          type: integer
          minimum: 0
          description: >-
            Optimistic-concurrency guard: the write is rejected with 409 if the
            stored version has moved on.
          example: 3
        allowRemovals:
          type: boolean
          default: false
          description: >-
            Confirms that nodes and outcome entries missing from this request
            should be deleted. Without it, a request that would destroy existing
            nodes or outcome lanes is rejected with 400 listing them by id. Set
            it only when you have read the current graph and mean to drop those
            parts.
          example: false
    CampaignWorkflowsReadResponse:
      type: object
      additionalProperties: false
      required:
        - success
        - workflow
      properties:
        success:
          type: boolean
          example: true
        workflow:
          oneOf:
            - $ref: '#/components/schemas/CampaignWorkflowsWorkflow'
            - type: 'null'
        warnings:
          type: array
          description: >-
            Everything about the graph that is worth knowing and was not worth
            refusing. Empty on a clean graph. Read this after a write: a 200
            means the graph was stored, not that every node will do something.
          items:
            $ref: '#/components/schemas/CampaignWorkflowsAdvisory'
    CampaignWorkflowsErrorResponse:
      type: object
      additionalProperties: {}
      required:
        - success
        - error
        - message
      properties:
        success:
          type: boolean
        error:
          type: string
          description: >-
            Machine-readable cause. Prefer branching on the HTTP status: this
            field spans two producers and is not a single closed set.
          example: VALIDATION
        code:
          type: string
          example: VALIDATION
        message:
          type: string
          example: Workflow graph must have a nodes object.
        requiredRole:
          description: 'Present on a role-based 403: the role the route requires.'
          example: agency_admin
        requiredScope:
          type: string
          description: 'Present on a scope-based 403: the API-key scope the route requires.'
          example: campaigns:write
    CampaignWorkflowsGraphNode:
      type: object
      additionalProperties: false
      required:
        - id
        - type
      properties:
        id:
          type: string
          example: tag_booked
        type:
          type: string
          description: >-
            Node action type. The campaign-builder canvas also renders a
            "Nurture" umbrella as a GROUP, but `group` is an editor-only visual
            construct, not a writable node type — saving a node of type `group`
            is rejected.
          enum:
            - if
            - switch
            - add-tag
            - remove-tag
            - set-field
            - move-stage
            - add-note
            - send-sms
            - send-email
            - book-appointment
            - trigger
        config:
          $ref: '#/components/schemas/CampaignWorkflowsNodeConfig'
        label:
          type: string
          description: >-
            Optional display name. Carried into execution history so a step
            reads as something other than its node type.
          example: Confirmation text
    CampaignWorkflowsGraphEdge:
      type: object
      additionalProperties: false
      required:
        - id
        - source
        - target
      properties:
        id:
          type: string
          example: e_booked
        source:
          type: string
          example: tag_booked
        target:
          type: string
          example: sms_reminder
        sourceHandle:
          type: string
          description: >-
            Branch handle for conditional or switch nodes (e.g. `then`, `else`,
            a switch case id, or `default`).
          example: then
    CampaignWorkflowsCampaignConfig:
      type: object
      description: >-
        Campaign-level settings saved with the workflow. Documented fields cover
        supported campaign controls; additional keys (including canvas-only
        presentation state) are preserved for forward-compatible options.
      additionalProperties: {}
      properties:
        callbackMode:
          oneOf:
            - type: string
              enum:
                - ai
                - human
            - type: 'null'
          description: >-
            Who handles a callback request. `null` reads the same as absent —
            not set.
          example: ai
        humanCallbackCalendarId:
          oneOf:
            - type: string
            - type: 'null'
        noteLanguage:
          type: string
          example: en
        calendarId:
          type: string
          example: calendar_primary
        cadenceId:
          type: string
          example: cadence_three_touch
        collectAddress:
          type: boolean
          example: true
        transferNumber:
          type: string
          example: '+14155550100'
        transferToAssignedUser:
          type: boolean
          example: false
        s2lDoubleDial:
          type: boolean
          example: false
        convertedTag:
          type: string
          example: booked
        aiNotes:
          type: object
          description: >-
            The "Subscribe to notes" toggles, one per outcome group. When an
            outcome's flag is true, the engine writes an AI call-summary note
            onto the GHL contact for that outcome.
          additionalProperties: false
          properties:
            converted:
              type: boolean
            opt_out:
              type: boolean
            callback:
              type: boolean
            human_needed:
              type: boolean
            followup_request:
              type: boolean
            no_action:
              type: boolean
    CampaignWorkflowsWorkflow:
      type: object
      additionalProperties: false
      required:
        - locationId
        - campaignId
        - nodes
        - edges
        - dispositionEntries
      properties:
        locationId:
          type: string
          example: loc_9f7a123
        campaignId:
          type: string
          example: campaign_speed_to_lead_123
        campaignConfig:
          $ref: '#/components/schemas/CampaignWorkflowsCampaignConfig'
        nodes:
          type: object
          description: Workflow nodes keyed by node id.
          additionalProperties:
            $ref: '#/components/schemas/CampaignWorkflowsGraphNode'
        edges:
          type: array
          description: Directed connections between workflow nodes.
          items:
            $ref: '#/components/schemas/CampaignWorkflowsGraphEdge'
        dispositionEntries:
          type: object
          description: >-
            Maps each call outcome to the id of the node that starts its action
            chain. Keys are uppercase outcome constants: SUCCESS/BOOKED
            (booked), CONFIRMED (appointment confirmed), RESCHEDULE (rebooked),
            TRANSFER (live-transferred), TAG_APPLIED (tag objective met),
            CALLBACK (asked for a callback), INFO_ONLY/FOLLOWUP_REQUESTED (asked
            for information), HUMAN_NEEDED (asked for a person),
            VOICEMAIL/NO_ACTION (no outcome), KILL_SWITCH (opted out),
            FAILED_TRANSFER (transfer did not connect). Which outcomes an agent
            can emit depends on its type; the legal set per agent type is
            returned as `dispositions` by GET /api/v2/projects/systems. The
            value is the id of a node in `nodes` — the first node of your action
            chain for that outcome. The platform composes the canonical handler
            for each outcome automatically, so just attach your actions here; do
            not build your own trigger/detector nodes to re-detect a standard
            outcome. 


            `INFO_ONLY` and `FOLLOWUP_REQUESTED` are two spellings of the same
            outcome ("asked for information") and are treated as interchangeable
            at run time: outbound agent types emit `INFO_ONLY` and the inbound
            receptionist emits `FOLLOWUP_REQUESTED`, and a chain wired under
            either spelling fires for both. Use whichever the systems endpoint
            lists for your agent type. 


            A key no agent type can emit is accepted but never fires — the
            response reports it as an `unknown_outcome` warning rather than
            rejecting the write, because graphs predating a rename rely on being
            readable.
          additionalProperties:
            type: string
        version:
          type: integer
          example: 3
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CampaignWorkflowsAdvisory:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      description: >-
        A non-fatal finding about the saved graph. Advisories never fail a
        request — an unfinished node is a legitimate draft — but each one marks
        something that will not behave the way it reads.
      properties:
        code:
          type: string
          enum:
            - node_incomplete
            - unsafe_context
            - unreachable
            - unknown_outcome
          description: >-
            `node_incomplete`: saved, but missing config it needs to do anything
            at runtime. `unsafe_context`: legal on this outcome but probably not
            what you want (the same advisory the builder shows an operator).
            `unreachable`: no outcome entry or trigger reaches it, so it never
            runs. `unknown_outcome`: the chain is wired to a disposition this
            agent type can never produce.
          example: node_incomplete
        nodeId:
          type: string
          example: sms_reminder
        outcome:
          type: string
          description: >-
            Present on `unknown_outcome`: the disposition key that will never
            fire.
          example: BOOKED_APPOINTMENT
        nodeType:
          type: string
          example: send-sms
        context:
          type: string
          description: >-
            Present on `unsafe_context`: the entry context that produced the
            finding.
          example: callback
        message:
          type: string
          example: sms_reminder.config.message is required.
    CampaignWorkflowsNodeConfig:
      type: object
      description: >-
        Node-type-specific settings. Documented fields cover supported workflow
        actions; additional keys are preserved for forward-compatible node
        options.
      additionalProperties: {}
      properties:
        signals:
          type: array
          items:
            type: string
          description: >-
            Custom boolean post-call signals a detector (`trigger`) node fires
            on. These are agent-specific post-call field names, not
            dispositions; the agent's available fields are returned as
            `postCallFields` by GET /api/v2/projects/systems, and you can define
            your own. There is no preset signal vocabulary.
          example:
            - asked_about_pricing
        tags:
          type: array
          items:
            type: string
          description: >-
            GHL tags an `add-tag`/`remove-tag` node applies. Use your own tag
            names here. The `tfu_ai_*` outcome tags (tfu_ai_booked,
            tfu_ai_standby, tfu_ai_human_needed, tfu_ai_kill_switch,
            tfu_ai_live_transfer, tfu_ai_failed_transfer, tfu_ai_cancelled) are
            applied automatically by the engine on the matching outcome and do
            not need to be set here.
          example:
            - booked_via_ai
        field:
          type: string
          description: >-
            Post-call field an `if`/`switch` node routes on (e.g. a boolean or
            category value extracted from the call).
          example: qualified
        op:
          type: string
          enum:
            - is_true
            - is_false
            - is_set
            - is_empty
            - equals
            - not_equals
            - contains
            - starts_with
            - in
            - gt
            - gte
            - lt
            - lte
        value:
          description: Value used by conditional nodes.
        cases:
          type: array
          items:
            $ref: '#/components/schemas/CampaignWorkflowsSwitchCase'
        pipelineId:
          type: string
          example: pipeline_sales
        stageId:
          type: string
          example: stage_booked
        fieldId:
          type: string
          example: custom_field_123
        note:
          type: string
          example: Booked from campaign call.
        message:
          type: string
          example: Thanks for calling. Our team will follow up shortly.
        subject:
          type: string
          example: Thanks for your call
        html:
          type: string
          example: <p>Thanks for your call.</p>
        calendarId:
          type: string
          example: calendar_primary
        collectAddress:
          type: boolean
          example: true
    CampaignWorkflowsSwitchCase:
      type: object
      additionalProperties: false
      required:
        - id
      description: >-
        A stored switch case: a stable synthetic id (the edge's `sourceHandle`)
        plus its test. Current graphs carry `condition`. `value` appears only on
        legacy graphs, where it is compared with `equals` against the switch
        node's `config.field`.
      properties:
        id:
          type: string
          example: c0
        condition:
          $ref: '#/components/schemas/CampaignWorkflowsCondition'
        value:
          description: >-
            Legacy stored form only. Migrated to `condition` on the next canvas
            edit.
          example: qualified
    CampaignWorkflowsCondition:
      type: object
      additionalProperties: false
      required:
        - field
        - op
      description: >-
        A single test against a post-call field. `value` is required for every
        operator except the unary ones (`is_true`, `is_false`, `is_set`,
        `is_empty`). The ordering operators (`gt`/`gte`/`lt`/`lte`) compare
        numerically and fail closed when either side is not a clean number.
      properties:
        field:
          type: string
          example: qualified
        op:
          type: string
          enum:
            - is_true
            - is_false
            - is_set
            - is_empty
            - equals
            - not_equals
            - contains
            - starts_with
            - in
            - gt
            - gte
            - lt
            - lte
          example: is_true
        value:
          description: Omitted for the unary operators. `in` takes an array.
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: 'Send your API key as `Authorization: Bearer YOUR_API_KEY`.'

````