> ## 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.

# Add actions to an outcome

> **The normal way to add steps.** Append one or more nodes to the end of an outcome's action chain. **Prefer this over the whole-graph PUT for ordinary edits.** You describe what should happen; the server derives the node ids, the edge ids, the branch handles and the canvas coordinates, which are the parts the PUT expects you to invent correctly.

Applied atomically: if any node in the list is rejected, nothing is written. Appending an action the chain already performs is a no-op rather than a duplicate, so a retry is safe.

Each entry is discriminated on `type`. Most types are `{ type, config }`. A `switch` router is `{ type: "switch", cases: [{ condition, actions }], default }` because one router produces a branch node plus a chain per case.

Unlike the PUT, per-node config IS required here — an action you are adding on purpose should be complete. Use the PUT if you want to save an unconfigured draft. A chain that already ends in a branch cannot be appended to; edit it in the builder.

Send an `Idempotency-Key` header if you may retry. This is the only operation here that creates, so a request that timed out is genuinely ambiguous. A repeat with the same key and the same body returns the original result instead of appending again; a repeat with the same key and a different body is rejected with 409.

Required API key scope: `campaign_workflows:write`.



## OpenAPI

````yaml /openapi.yaml post /api/agent-builder/agents/{id}/campaign/workflow/outcomes/{outcome}/nodes
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/outcomes/{outcome}/nodes:
    post:
      tags:
        - Campaign Workflows
      summary: Add actions to an outcome
      description: >-
        **The normal way to add steps.** Append one or more nodes to the end of
        an outcome's action chain. **Prefer this over the whole-graph PUT for
        ordinary edits.** You describe what should happen; the server derives
        the node ids, the edge ids, the branch handles and the canvas
        coordinates, which are the parts the PUT expects you to invent
        correctly.


        Applied atomically: if any node in the list is rejected, nothing is
        written. Appending an action the chain already performs is a no-op
        rather than a duplicate, so a retry is safe.


        Each entry is discriminated on `type`. Most types are `{ type, config
        }`. A `switch` router is `{ type: "switch", cases: [{ condition, actions
        }], default }` because one router produces a branch node plus a chain
        per case.


        Unlike the PUT, per-node config IS required here — an action you are
        adding on purpose should be complete. Use the PUT if you want to save an
        unconfigured draft. A chain that already ends in a branch cannot be
        appended to; edit it in the builder.


        Send an `Idempotency-Key` header if you may retry. This is the only
        operation here that creates, so a request that timed out is genuinely
        ambiguous. A repeat with the same key and the same body returns the
        original result instead of appending again; a repeat with the same key
        and a different body is rejected with 409.


        Required API key scope: `campaign_workflows:write`.
      operationId: add-campaign-workflow-nodes
      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
        - name: outcome
          in: path
          required: true
          schema:
            type: string
            minLength: 1
          description: >-
            The call outcome whose chain to append to. Accepts the raw
            disposition (`SUCCESS`), any case (`success`), or the label the
            dashboard shows (`On callback`). Rejected with 400 if the agent's
            type can never emit it — see `dispositions` from GET
            /api/v2/projects/systems.
          example: SUCCESS
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 255
          description: >-
            Optional retry key for create-style operations. Reusing the same key
            with the same request returns the original result; reusing it with a
            different request returns 409.
          example: workflow-append-20260806-001
      requestBody:
        description: Request payload. _Detailed schema pending — see the route handler._
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignWorkflowsAppendNodesRequest'
              type: object
              additionalProperties: {}
            examples:
              default:
                value:
                  nodes:
                    - type: add-tag
                      config:
                        tags:
                          - booked_via_ai
                    - type: send-sms
                      config:
                        message: Thanks for booking! See you soon.
        required: true
      responses:
        '200':
          description: The updated 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: 3
                      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 POST \
              --url https://api.teamfollowup.ai/api/agent-builder/agents/{id}/campaign/workflow/outcomes/{outcome}/nodes \
              --header 'Authorization: Bearer YOUR_API_KEY' \
              --header 'Content-Type: application/json' \
              --data '{
              "nodes": [
                {
                  "type": "add-tag",
                  "config": {
                    "tags": [
                      "booked_via_ai"
                    ]
                  }
                },
                {
                  "type": "send-sms",
                  "config": {
                    "message": "Thanks for booking! See you soon."
                  }
                }
              ]
            }'
components:
  schemas:
    CampaignWorkflowsAppendNodesRequest:
      type: object
      additionalProperties: false
      required:
        - nodes
      properties:
        nodes:
          type: array
          minItems: 1
          description: >-
            Appended in order to the end of the outcome chain, as one atomic
            write.
          items:
            oneOf:
              - $ref: '#/components/schemas/CampaignWorkflowsSwitchInput'
              - $ref: '#/components/schemas/CampaignWorkflowsActionInput'
        expectedVersion:
          type: integer
          minimum: 0
          description: >-
            Optimistic-concurrency guard: rejected with 409 if the stored
            version has moved on.
          example: 3
    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
    CampaignWorkflowsSwitchInput:
      type: object
      additionalProperties: false
      required:
        - type
        - cases
      description: >-
        A router to add. One router produces a branch node plus a linear chain
        per case, which is why the cases carry their own actions rather than
        being wired separately.
      properties:
        type:
          type: string
          enum:
            - switch
        cases:
          type: array
          minItems: 1
          description: Checked in order; the first matching condition takes its chain.
          items:
            type: object
            additionalProperties: false
            required:
              - condition
              - actions
            properties:
              condition:
                $ref: '#/components/schemas/CampaignWorkflowsCondition'
              actions:
                type: array
                minItems: 1
                description: >-
                  A route that does nothing is not a route — omit it and let
                  those calls fall to default.
                items:
                  $ref: '#/components/schemas/CampaignWorkflowsActionInput'
        default:
          type: object
          additionalProperties: false
          required:
            - actions
          description: >-
            Runs when no case matched. Omit it to let unmatched calls do
            nothing.
          properties:
            actions:
              type: array
              minItems: 1
              items:
                $ref: '#/components/schemas/CampaignWorkflowsActionInput'
    CampaignWorkflowsActionInput:
      type: object
      additionalProperties: false
      required:
        - type
      description: >-
        One action to add. No id, no coordinates, no edges — the server derives
        all three from where the action lands in the chain.
      properties:
        type:
          type: string
          enum:
            - add-tag
            - remove-tag
            - set-field
            - move-stage
            - add-note
            - send-sms
            - send-email
            - book-appointment
          description: >-
            Branch types (`if`, `switch`) are not actions. Add a router with the
            `switch` variant; build an `if` in the builder.
          example: add-tag
        config:
          $ref: '#/components/schemas/CampaignWorkflowsNodeConfig'
    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.
    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.
    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
    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
    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
    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
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: 'Send your API key as `Authorization: Bearer YOUR_API_KEY`.'

````