Getting StartedAI in WebexBlogSupport
Log inSign up
Home
Webex Contact Center
  • Overview
  • Guides
  • API REFERENCE
  • AI
  • Campaign Management
  • Configuration
  • Data
  • Desktop
  • Flow Orchestration
  • Journey
  • Media And Routing
  • Changelog
  • SDK
  • Widgets
  • Customer Journey Data Service
  • AI Assistant for Developers
  • Webhooks
  • Contact Center Sandbox
  • Using Webhooks
  • Troubleshoot the API
  • Beta Program
  • Webex Status API
  • Contact Center Service Apps
  • FAQs

Webex Contact Center

Getting Started with Flow Orchestration

Learn how to author a Webex Contact Center flow as a JSON document and import it through the Flow Orchestration APIs — entirely programmatically, without the UI.

anchorIntroduction

anchor

A flow is a directed graph that describes the logical steps of a contact-center interaction. Each box on the canvas is a node that runs one activity — play a prompt, queue a contact, end the interaction — and the arrows between boxes are edges that decide which node runs next.

The Flow Orchestration APIs let you discover the activities and events available in your project, assemble those nodes and edges into a Flow JSON document, validate it, and import it as a draft on the tenant — the same headless discover → assemble → validate → import loop that AI-assisted flow authoring builds on.

A Flow document is made up of these pieces:

  • Start node — the single entry point. It has activityType: "start" and properties.activityName: "start". Its properties.flowType carries the trigger binding (eventSourceName, eventClassificationName, eventSpecificationName); for inbound telephony the eventSpecificationName is ContactStartWorkflow.
  • Action nodes — the work the flow does. They have activityType: "action" and name the activity in properties.activityName (for example play-message).
  • End node — terminates the interaction. It has activityType: "end", for example properties.activityName: "disconnect-contact".
  • Edges — connect nodes and select which branch fires through the edge's condition (and, for a branching port, the edge's properties).
  • Variables — named values the flow reads and writes.
  • Event flow — a separate node/edge graph that runs when a bound event fires (this is how a global error handler is modeled).
  • Preferences — flow-level settings.

Activity names, event names, and input fields are discovered per project at runtime — this guide uses the canonical values returned for a telephony project, but you should read the values for your own project from the discovery endpoints in Before you begin.

The Flow Orchestration API is in Beta.

anchorBefore you begin

anchor

You need:

  • A Webex Contact Center administrator access token. See Authentication for how to obtain one.
  • Scopes: cjp:config_read for the discovery calls, and cjp:config_write for validate and import.
  • The base URL: https://api.produs1.ciscoccservice.com.
  • Your organization ID ({orgId}) and the project ID ({projectId}). The project ID is a system-generated value that is the same across orgs and environments — always use 5e5c9ad6d61f870d6d778c1b.

All requests send the token in the Authorization header:

Authorization: Bearer <YOUR_ACCESS_TOKEN>

The examples below use the shell variables ${TOKEN} and ${ORG_ID} for these values.

Discover activities

A flow can only reference activities that exist in your project. List them — along with their group, input fields, output ports, and the JSON Schema for their inputs — with listActivityDefinitions. The response is self-describing and sufficient on its own to construct nodes:

curl -sS \
  "https://api.produs1.ciscoccservice.com/${ORG_ID}/project/5e5c9ad6d61f870d6d778c1b/v2/activities" \
  -H "Authorization: Bearer ${TOKEN}"

Each activity reports a group of start, action, event, or end, and an activityName in kebab-case. Use the group to pick the activities you need:

  • Start — the activity with group: "start". Its activityName is start.
  • Play a prompt — play-message (group: "action").
  • End the flow — the activity with group: "end", for example disconnect-contact.

To inspect one activity's full input/output/port schema, call describeActivity with the kebab-case activity name:

curl -sS \
  "https://api.produs1.ciscoccservice.com/${ORG_ID}/project/5e5c9ad6d61f870d6d778c1b/v2/activities/play-message" \
  -H "Authorization: Bearer ${TOKEN}"

When an input takes a value from a constrained set (a queue, an audio file, a team), resolve the allowed values with getActivityInputChoices. It supports four modes: list (all values), search (type-ahead by query), validate (check one value), and dynamic (cascading choices that depend on sibling inputs via parent_inputs). The endpoint is only valid for inputs that expose choices — an input whose describeActivity definition includes allowedValues or a choicesEndpoint; calling it for any other input returns 400:

curl -sS \
  "https://api.produs1.ciscoccservice.com/${ORG_ID}/project/5e5c9ad6d61f870d6d778c1b/v2/activities/queue-contact/inputs/queue/choices?mode=list" \
  -H "Authorization: Bearer ${TOKEN}"
Discover events

An event flow can only react to events your project exposes. List them with listEventSpecifications:

curl -sS \
  "https://api.produs1.ciscoccservice.com/${ORG_ID}/project/5e5c9ad6d61f870d6d778c1b/v2/event-specifications" \
  -H "Authorization: Bearer ${TOKEN}"

Each result's name is what you put into an event node's properties.eventSpecificationName. Find the global-error event in this list and use its name for the event node in Add a global error handler.

anchorFlow document structure

anchor

A Flow document has a small set of top-level fields plus arrays of nodes, edges, and variables, a single event-flow object, and an array of preferences.

Top-level fields
PropertyTypeNotes
namestringName of the flow. Must be unique in the project unless you import with overwrite=true.
flowTypeenumFLOW or SUBFLOW.
contactTypestringChannel type — for example telephony, customMessaging, workItem, genericAction.
descriptionstringHuman-readable description.
versionintegerMonotonically increasing document version. Server-assigned; do not set it on import.
statusenumDraft or Published. Server-managed.
nodesarrayActivity nodes in the main flow process. Must include exactly one start node.
edgesarrayEdges connecting nodes in the main flow process.
variablesarrayFlow variables.
eventFlowsobjectA single event-handler process (nodes and edges) that runs when a bound event fires.
preferencesarrayFlow-level preferences.
Node

A node represents one activity instance. Nodes appear in nodes[] and in the event flow's nodes[].

FieldNotes
idStable, unique node identifier within the flow.
nameNode name. Referenced by edges (from/to) and used as the merge key for PATCH (upsert_nodes, remove_node_names).
activityTypeCategory of node: start, action, event, or end. A flow must contain exactly one start node.
propertiesActivity configuration. Always includes properties.activityName (the activity this node runs, matching an activityName from listActivityDefinitions); the remaining keys are the activity's input values and depend on the activity definition.
Edge
FieldNotes
idStable, unique edge identifier within the flow. Also the merge key for PATCH (upsert_edges, remove_edge_keys).
fromSource node name.
toTarget node name.
conditionBranch condition the edge fires on. Aliases done → out and defaultBranch → default are normalized server-side.
propertiesOptional edge properties — for example, the condition value for a branching port.
Variable
FieldNotes
nameVariable name.
typeData type, e.g. STRING, INTEGER, BOOLEAN.
valueDefault value, encoded as a string.
descriptionHuman-readable description.
isCADtrue if exposed as Call-Associated Data.
isAgentEditabletrue if agents can edit the value at runtime.
isReportabletrue if included in reporting.
isSecuretrue if the value is sensitive and must be masked in logs and reports.
Event flow

The eventFlows field is a single object with its own nodes and edges:

FieldNotes
nodesActivity nodes in the event-handler process.
edgesEdges in the event-handler process.

An event handler begins with an event node (activityType: "event") whose properties.eventSpecificationName names the event it reacts to (along with eventSourceName and eventClassificationName). A global error handler is simply an event flow whose event node binds the global-error event. You build its nodes and edges exactly like the main flow.

Preference

A preference is a flow-level setting:

FieldNotes
namePreference name, e.g. hideSecureCADWarning.
typePreference value type, e.g. Boolean.
valueValue, encoded as a string.
Ports and conditions

A minimal flow is three nodes wired in a line:

NewContact  --(condition: out)-->  WelcomeMessage  --(condition: default)-->  DisconnectContact

An activity exposes named output ports (discoverable via describeActivity as outputPorts[].name, for example out or onTimeout). Ports are discovery metadata — they are not named directly on the edge. Instead, an edge selects which branch it represents through its condition, and for a branching port the specific value goes in the edge's properties.

When you import or validate a flow, the server enforces three rules. Keep them in mind while assembling the document by hand:

  1. Every edge's from and to must reference a node name that exists in the same process (the main flow, or the event flow).
  2. An edge's condition must correspond to a real output of the source node's activity — it matches one of the activity's outputPorts[].name, after the aliases done → out and defaultBranch → default are normalized.
  3. Node name values and edge id values must be unique within a process, and the main process must contain exactly one start node.

anchorBuild a basic flow

anchor

This first flow greets the caller with a prompt and then ends:

NewContact  -->  WelcomeMessage  -->  DisconnectContact
Step 1 — Assemble the flow document

Save the following as flow.json. The activity names (start, play-message, disconnect-contact) are the canonical values for a telephony project; confirm yours with Discover activities.

{
  "name": "SamplePlayMessageFlow",
  "flowType": "FLOW",
  "contactType": "telephony",
  "description": "Minimal sample flow: greet the caller, then disconnect.",
  "variables": [],
  "nodes": [
    {
      "id": "node-start",
      "name": "NewContact",
      "activityType": "start",
      "properties": {
        "activityName": "start",
        "flowType": {
          "eventSourceName": "WebexContactCenter",
          "eventClassificationName": "VoiceInteractions",
          "eventSpecificationName": "ContactStartWorkflow"
        }
      }
    },
    {
      "id": "node-welcome",
      "name": "WelcomeMessage",
      "activityType": "action",
      "properties": {
        "activityName": "play-message",
        "prompt": {
          "promptType": "text",
          "text": "Thank you for calling. Goodbye.",
          "textType": "text"
        }
      }
    },
    {
      "id": "node-end",
      "name": "DisconnectContact",
      "activityType": "end",
      "properties": {
        "activityName": "disconnect-contact"
      }
    }
  ],
  "edges": [
    {
      "id": "edge-1",
      "from": "NewContact",
      "to": "WelcomeMessage",
      "condition": "out",
      "properties": {}
    },
    {
      "id": "edge-2",
      "from": "WelcomeMessage",
      "to": "DisconnectContact",
      "condition": "default",
      "properties": {}
    }
  ],
  "preferences": [
    { "name": "hideSecureCADWarning", "type": "Boolean", "value": "true" }
  ]
}

The node name values (NewContact, WelcomeMessage, DisconnectContact) are human-readable labels and can be anything unique within the process — it is the activityType and properties.activityName that determine what each node does. Note how the three rules from Ports and conditions hold: the two edges reference existing node names, each fires on a condition the source activity exposes, and the node names and edge IDs are all unique.

The play-message node's properties.prompt shown here is the shape the API returns in its examples; the exact inputs an activity accepts are defined by its describeActivity schema, so confirm the input names for your project.

Step 2 — Validate (dry run)

Validate the document before persisting it. This does not create anything:

curl -sS -X POST \
  "https://api.produs1.ciscoccservice.com/${ORG_ID}/project/5e5c9ad6d61f870d6d778c1b/v2/flows:validate" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  --data @flow.json

A valid document returns:

{
  "valid": true,
  "errors": []
}

If something is wrong, validation tells you why:

{
  "valid": false,
  "errors": [
    {
      "path": "/edges/1/condition",
      "code": "INVALID_PORT",
      "message": "Activity 'play-message' does not expose a port named 'onTimeout'."
    }
  ]
}

Validation is more lenient than import: a flow that passes :validate can still be rejected by :import — for example, a document missing its single start node. Treat a successful validation as necessary, not sufficient.

Step 3 — Import the draft

Import the document. This creates the flow in Draft state and returns its metadata, including the assigned flow.id:

curl -sS -X POST \
  "https://api.produs1.ciscoccservice.com/${ORG_ID}/project/5e5c9ad6d61f870d6d778c1b/v2/flows:import?overwrite=false&flowType=FLOW" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  --data @flow.json

A successful import returns 201 Created. The flow document is returned under flow, alongside any non-blocking preflightWarnings raised while persisting it:

{
  "flow": {
    "id": "661c7bc712eaf357de7e4aeb",
    "orgId": "8eb7da9a-c81c-4d13-b08b-38fdeb7330d8",
    "version": 0,
    "flowType": "FLOW",
    "name": "SamplePlayMessageFlow",
    "description": "Minimal sample flow: greet the caller, then disconnect.",
    "status": "Draft",
    "createdBy": "user@example.com",
    "createdDate": "2026-01-15T09:12:00.000Z",
    "lastModifiedBy": "user@example.com",
    "lastModifiedDate": "2026-01-15T09:12:00.000Z"
  },
  "preflightWarnings": [],
  "preflightWarningsCount": 0
}

Two error responses are worth knowing:

  • 409 Conflict — a flow with the same name already exists and overwrite is false. Re-import with overwrite=true or choose a new name.
  • 422 Unprocessable Entity — the document failed validation. The body carries the same { "valid": false, "errors": [...] } shape as the validate call.

The server assigns the flow's flow.id and authoritative flow.version; do not rely on the version you send in the body (you manage version explicitly only when saving a draft — see Modify a draft incrementally).

Step 4 — Verify

Fetch the persisted draft by its flow.id to confirm it was stored as expected:

curl -sS \
  "https://api.produs1.ciscoccservice.com/${ORG_ID}/project/5e5c9ad6d61f870d6d778c1b/v2/flows/661c7bc712eaf357de7e4aeb" \
  -H "Authorization: Bearer ${TOKEN}"

You can also re-validate the stored draft with …/v2/flows/{flowId}:validate?versionId=draft, or export it for backup with …/v2/flows/{flowId}:export?version=latest.

Validation errors

When valid is false (or you receive a 422), each entry in errors[] points at the offending field:

FieldNotes
pathJSON Pointer into the document you sent, e.g. /nodes/2/properties/queue.
codeStable, machine-readable code. Treat it as an opaque string and key your handling off it.
messageHuman-readable explanation — surface this to the author.

Common causes:

codeCause
UNKNOWN_ACTIVITYA node's properties.activityName is not in listActivityDefinitions.
INVALID_PORTAn edge's condition is not an output port the source activity exposes.
UNKNOWN_QUEUEAn input value is not in the resolved choice set for that input.

This is not an exhaustive list of codes; always read message for the specifics.

anchorAdd a global error handler

anchor

This second flow keeps the same main process and adds an event flow that runs when the global-error event fires. The handler ends the contact:

main:    NewContact  -->  WelcomeMessage  -->  DisconnectContact
handler: GlobalErrorHandling  -->  EndOnError      (runs on the global-error event)
Step 1 — Add the event flow

The main nodes and edges are unchanged. eventFlows is a single object with its own nodes and edges. The handler's first node is an event node whose properties.eventSpecificationName is the event name you found in Discover events — here, GlobalErrorHandling.

{
  "name": "SamplePlayMessageFlowWithErrorHandler",
  "flowType": "FLOW",
  "contactType": "telephony",
  "description": "Sample flow with a global error handler event flow.",
  "variables": [],
  "nodes": [
    {
      "id": "node-start",
      "name": "NewContact",
      "activityType": "start",
      "properties": {
        "activityName": "start",
        "flowType": {
          "eventSourceName": "WebexContactCenter",
          "eventClassificationName": "VoiceInteractions",
          "eventSpecificationName": "ContactStartWorkflow"
        }
      }
    },
    {
      "id": "node-welcome",
      "name": "WelcomeMessage",
      "activityType": "action",
      "properties": {
        "activityName": "play-message",
        "prompt": {
          "promptType": "text",
          "text": "Thank you for calling. Goodbye.",
          "textType": "text"
        }
      }
    },
    {
      "id": "node-end",
      "name": "DisconnectContact",
      "activityType": "end",
      "properties": {
        "activityName": "disconnect-contact"
      }
    }
  ],
  "edges": [
    {
      "id": "edge-1",
      "from": "NewContact",
      "to": "WelcomeMessage",
      "condition": "out",
      "properties": {}
    },
    {
      "id": "edge-2",
      "from": "WelcomeMessage",
      "to": "DisconnectContact",
      "condition": "default",
      "properties": {}
    }
  ],
  "eventFlows": {
    "nodes": [
      {
        "id": "event-global-error",
        "name": "GlobalErrorHandling",
        "activityType": "event",
        "properties": {
          "activityName": "event",
          "eventSourceName": "WebexContactCenter",
          "eventClassificationName": "VoiceInteractions",
          "eventSpecificationName": "GlobalErrorHandling"
        }
      },
      {
        "id": "event-end",
        "name": "EndOnError",
        "activityType": "end",
        "properties": {
          "activityName": "disconnect-contact"
        }
      }
    ],
    "edges": [
      {
        "id": "event-edge-1",
        "from": "GlobalErrorHandling",
        "to": "EndOnError",
        "condition": "out",
        "properties": {}
      }
    ]
  },
  "preferences": [
    { "name": "hideSecureCADWarning", "type": "Boolean", "value": "true" }
  ]
}

Two things to note:

  • The main process and the event flow are independent scopes. Node names, edge IDs, and conditions only have to be unique within their own process, which is why the handler can reuse the disconnect-contact activity under a new node name (EndOnError).
  • The event node is the handler's entry point: it binds the event via properties.eventSpecificationName, and an edge leads from it into the handling activities. There is no top-level event field — the binding lives entirely on the event node.
Step 2 — Validate and import

Validate and import exactly as in Build a basic flow — the same …:validate and …:import calls apply. Import validates the entire document, including the event flow, so a problem inside eventFlows surfaces in errors[] with a path such as /eventFlows/nodes/0/properties/eventSpecificationName (unknown event) or /eventFlows/nodes/1/properties/activityName.

anchorModify a draft incrementally

anchor

Once a draft exists you rarely rewrite the whole document. There are two ways to change it.

Save a flow draft

Replace the whole draft with the Save Flow Draft operation — POST …/v2/flows/{flowId}?expectedVersion=N. Pass expectedVersion to enable optimistic locking; the request fails with 409 Conflict if the server's draft version has moved on. Omit it to skip the check. The response is the same metadata envelope as import (flow, preflightWarnings, preflightWarningsCount), with an incremented flow.version.

Patch a flow draft

Apply a partial change with the Patch Flow Draft operation — PATCH …/v2/flows/{flowId}. The body is a patch contract; the merge happens server-side, is idempotent, and is re-validated, so the draft is never left broken:

{
  "upsert_nodes": [
    {
      "id": "node-hold",
      "name": "HoldMessage",
      "activityType": "action",
      "properties": {
        "activityName": "play-message",
        "prompt": {
          "promptType": "text",
          "text": "Thanks for calling. Please hold.",
          "textType": "text"
        }
      }
    }
  ],
  "upsert_edges": [
    {
      "id": "edge-3",
      "from": "WelcomeMessage",
      "to": "HoldMessage",
      "condition": "out",
      "properties": {}
    }
  ],
  "remove_node_names": [],
  "remove_edge_keys": []
}

upsert_nodes matches existing nodes by name and upsert_edges matches by id — present items are replaced, new ones are added. remove_node_names deletes nodes by name and remove_edge_keys deletes edges by id; if you remove a node, remove the edges that reference it in the same patch or the merged document fails validation. A PATCH body may also carry top-level overrides such as name and description.

This is what makes a hand-authored flow expandable: start from the atomic flow above, then grow it one patch at a time.

anchorBuild a custom function

anchor

A custom function is a small piece of user-authored code (JavaScript or Python) that a flow can call to transform data — parse a JSON payload, normalize a phone number, look up a value. Functions are managed by the Custom Functions API. Unlike the flow endpoints, its paths are not scoped to a project:

  • The base URL is https://api.produs1.ciscoccservice.com.
  • Function paths are /v1/{orgId}/functions… — there is no {projectId} segment.
  • Scopes: cjp:config_read to list, get, and export; cjp:config_write to create, update, test, publish, lock, unlock, and import. The Authorization: Bearer ${TOKEN} header is the same as before.

This walkthrough builds a parseContact function that takes a contact record and returns the caller's phone digits and area code, then tests and publishes it. Once published, a function can be referenced from a flow.

A function exports a single handle entry point. It reads its declared inputs from request.inputs, writes its result to response.data, and returns the response:

export const handle = async (request, response) => {
  const phoneDigits = String(request.inputs.contact.phone).replace(/\D/g, "");
  response.data = {
    phoneDigits,
    areaCode: phoneDigits.slice(0, 3)
  };
  return response;
};
Step 1 — Create the function

sourceCode is sent as an escaped string. inputs[] declares each input's name, dataType (one of boolean, datetime, decimal, integer, json, string), and a sample value. outputs is a stringified JSON object mapping each output name to a sample value. language is js or py. selectedRuntime is case-sensitive and defaults to the highest supported runtime for the language (for example nodejs22.x for js, or python3.13 for py). timeoutInSec caps execution time.

curl -sS -X POST \
  "https://api.produs1.ciscoccservice.com/v1/${ORG_ID}/functions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "name": "parseContact",
    "description": "Parses a contact record: strips the phone to digits and extracts the area code.",
    "language": "js",
    "selectedRuntime": "nodejs22.x",
    "timeoutInSec": 3,
    "sourceCode": "export const handle = async (request, response) => {\n  const phoneDigits = String(request.inputs.contact.phone).replace(/\\D/g, \"\");\n  response.data = { phoneDigits, areaCode: phoneDigits.slice(0, 3) };\n  return response;\n};",
    "inputs": [
      {
        "name": "contact",
        "dataType": "json",
        "value": "{\"name\":\"John Doe\",\"phone\":\"(415) 555-0132\"}"
      }
    ],
    "outputs": "{\"phoneDigits\":\"4155550132\",\"areaCode\":\"415\"}"
  }'

A successful create returns 201 Created. The source code is returned under fnCode and the server-side metadata — including the assigned id and a Draft status — under fnMetadata:

{
  "fnCode": "export const handle = async (request, response) => {\n  const phoneDigits = String(request.inputs.contact.phone).replace(/\\D/g, \"\");\n  response.data = { phoneDigits, areaCode: phoneDigits.slice(0, 3) };\n  return response;\n};",
  "fnMetadata": {
    "id": "64f1b2c3d4e5f6a7b8c9d0e1",
    "orgId": "8eb7da9a-c81c-4d13-b08b-38fdeb7330d8",
    "name": "parseContact",
    "description": "Parses a contact record: strips the phone to digits and extracts the area code.",
    "language": "js",
    "selectedRuntime": "nodejs22.x",
    "status": "Draft",
    "timeoutInSec": 3,
    "tagVersionMap": {},
    "lockedBy": "",
    "createdBy": "user@example.com",
    "createdDate": "2026-01-15T09:12:00.000Z",
    "lastModifiedBy": "user@example.com",
    "lastModifiedDate": "2026-01-15T09:12:00.000Z"
  }
}
Step 2 — Test it

Run the function with a test payload. inputs is a JSON object whose keys match the declared input names — here, the John Doe contact:

curl -sS -X POST \
  "https://api.produs1.ciscoccservice.com/v1/${ORG_ID}/functions/64f1b2c3d4e5f6a7b8c9d0e1:test" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "inputs": {
      "contact": {
        "name": "John Doe",
        "phone": "(415) 555-0132"
      }
    }
  }'

The response carries the function's response.data in result, plus any captured logs and the execution time:

{
  "result": {
    "phoneDigits": "4155550132",
    "areaCode": "415"
  },
  "logs": "",
  "durationMs": 12
}

The test call implicitly publishes the latest draft before running, so it always executes your most recent source code.

Step 3 — Publish it

Publish the draft under one or more tags (Dev, Test, Latest, Live) to make a version addressable:

curl -sS -X POST \
  "https://api.produs1.ciscoccservice.com/v1/${ORG_ID}/functions/64f1b2c3d4e5f6a7b8c9d0e1:publish" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{ "tags": ["Live"], "comment": "Initial release." }'

Publishing returns the same { "fnCode", "fnMetadata" } envelope, with the new tag reflected in fnMetadata.tagVersionMap. The published function can now be referenced from a flow.

Function lifecycle reference

The rest of the lifecycle follows the same pattern:

  • GET …/v1/{orgId}/functions and GET …/v1/{orgId}/functions/{id} — list and inspect.
  • PUT …/v1/{orgId}/functions/{id} — update the draft (same body shape as create).
  • …/v1/{orgId}/functions/{id}:export returns the function as a downloadable file (fileName, contentType, and base64 content), and …/v1/{orgId}/functions:import accepts that file as a multipart upload — together they move a function between environments.
  • …/v1/{orgId}/functions/{id}:lock and :unlock coordinate concurrent edits and return a status.

anchorRestrictions

anchor
  1. A flow's main process must contain exactly one start node (activityType: "start"). This is the canonical case where :validate can pass but :import rejects the document.
  2. A flow name must be unique within the project unless you import with overwrite=true; otherwise import returns 409 Conflict.
  3. Validation is more lenient than import — passing :validate does not guarantee that :import will succeed.
  4. Node name values and edge id values must be unique within their process, and an edge may only reference nodes in the same process (no edges between the main flow and the event flow).
  5. When patching a draft, edges that reference a removed node must be removed in the same patch, or the merged document fails validation.
  6. A custom function name must be unique within the organization; a duplicate name returns 400.
  7. Save and patch requests that pass expectedVersion fail with 409 Conflict if the server-side draft version has moved on.

anchorRecommendations and best practices

anchor
  • Discover before you author. List activities with listActivityDefinitions and events with listEventSpecifications, and use the returned kebab-case activityName values and event names rather than hard-coding them — the discovery responses are self-describing.
  • Validate before importing, but treat a successful :validate as necessary, not sufficient; always handle a possible 422 on :import.
  • Resolve constrained inputs (queues, audio files, teams) with getActivityInputChoices instead of guessing values, to avoid UNKNOWN_* validation errors.
  • Use PATCH for incremental edits, and pass expectedVersion to enable optimistic locking when multiple editors or automations touch the same draft.
  • Key your error handling off the stable code field; treat it as opaque and surface message to the author.
  • For custom functions, test (:test) before publishing, and publish under explicit tags (Dev/Test/Latest/Live) so versions stay addressable.
  • Keep sensitive values in isSecure variables so they are masked in logs and reports.

anchorNext steps

anchor
  • Publish. This guide stops at the validated draft. Publishing a draft (and the lock/unlock that goes with editing) is a separate step in the Flows API — see the Flow Orchestration API reference.
  • Expand the flow. Discover more activities with listActivityDefinitions, resolve their inputs with getActivityInputChoices, and add them with PATCH. Routing activities (for example, queueing a contact) follow the same node/edge pattern shown here.
  • Automate authoring. Because a flow is just a Flow JSON document validated by the …:validate endpoint, the discover → assemble → validate → import loop in this guide is the same foundation that AI-assisted flow authoring builds on.
In This Article
  • Introduction
  • Before you begin
  • Flow document structure
  • Build a basic flow
  • Add a global error handler
  • Modify a draft incrementally
  • Build a custom function
  • Restrictions
  • Recommendations and best practices
  • Next steps

Connect

Support

Developer Community

Developer Events

Contact Sales

Handy Links

Webex Ambassadors

Webex App Hub

Resources

Open Source Bot Starter Kits

Download Webex

DevNet Learning Labs

Terms of Service

Privacy Policy

Cookie Policy

Trademarks

© 2026 Cisco and/or its affiliates. All rights reserved.