> Source: https://wexa.ai/docs/automation/create-process-flow

# create-process-flow

[Can be held for approval](/docs/sdks-and-apis/how-every-tool-call-works#lifecycle-and-governance)

Create a process flow: an empty shell, or a full `manifest` that provisions the process flow, its agents and wiring in one call. Needs the `fabric:orchestrate.write` grant.

**Create a process flow**

**Wexa MCP server**

```bash
curl -sS -X POST https://fabric.wexa.ai/mcp/$FABRIC_PROJECT_ID \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "Mcp-Session-Id: $MCP_SESSION_ID" \
  -H "content-type: application/json" -H "accept: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create-process-flow","arguments":{"name":"docs-fixture-scratch-flow-1790023297678","description":"Created to capture delete-process-flow, then deleted."}}}'
```

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000209\",\"result\":{\"process_flow_id\":\"6ab196817566f41b5c28e33a\",\"flow\":{\"_id\":\"6ab196817566f41b5c28e33a\",\"name\":\"docs-fixture-scratch-flow-1790023297678\",\"description\":\"Created to capture delete-process-flow, then deleted.\"}}}"
    }
  ],
  "isError": false
}
```

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/create-process-flow \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"docs-fixture-scratch-flow-1790023297678","description":"Created to capture delete-process-flow, then deleted."}'
```

```json
{
  "lifecycle_id": "qlc_000209",
  "result": {
    "process_flow_id": "6ab196817566f41b5c28e33a",
    "flow": {
      "_id": "6ab196817566f41b5c28e33a",
      "name": "docs-fixture-scratch-flow-1790023297678",
      "description": "Created to capture delete-process-flow, then deleted."
    }
  }
}
```

**TypeScript SDK**

```ts
import { Fabric } from '@wexa-fabric/sdk'

const fabric = new Fabric()
const result = await fabric.createProcessFlow({
  name: 'docs-fixture-scratch-flow-1790023297678',
  description: 'Created to capture delete-process-flow, then deleted.'
})
```

```ts
// result is the payload itself, with no `.result` to read
{
  process_flow_id: '6ab196817566f41b5c28e33a',
  flow: {
    _id: '6ab196817566f41b5c28e33a',
    name: 'docs-fixture-scratch-flow-1790023297678',
    description: 'Created to capture delete-process-flow, then deleted.'
  }
}
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
out = fabric.create_process_flow(
    name="docs-fixture-scratch-flow-1790023297678",
    description="Created to capture delete-process-flow, then deleted.",
)
```

```python
# out is the result, already unwrapped
{
    "process_flow_id": "6ab196817566f41b5c28e33a",
    "flow": {
        "_id": "6ab196817566f41b5c28e33a",
        "name": "docs-fixture-scratch-flow-1790023297678",
        "description": "Created to capture delete-process-flow, then deleted."
    }
}
```

## Parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `description` | string | Optional | Description of the flow. |
| `flow_name` | string | Optional | Only with `manifest`. Overrides `manifest.flow.name`. |
| `kb_tags` | array | Optional | Only used with `manifest`. Sets `manifest.knowledge_base.tags` and REPLACES (never merges with) the context of every agent with `has_knowledge_base` true. Entries are tag tokens, never sentences; see More details. |
| `manifest` | object | Optional | The process flow, its agents and the wiring in one payload; see [the manifest](#the-manifest). Supply this, or `name` for a bare process flow. Validated strictly: an unrecognized field is rejected. |
| `mode` | string | Optional | Only used with `manifest`. `create` (default) refuses a taken name with a 409. `create_if_absent` returns the existing process flow untouched. `upsert` replaces its agents. |
| `name` | string | Optional | Names the process flow when you pass no `manifest`; required then. |
| `role` | string | Optional | Role of the flow. |

**Errors**

- `ValidationError` (400 or 422): an invalid manifest, skill ids not in this project, a skilled agent with no skills, or no `name` in shell mode. See [the manifest](#the-manifest).
- `ValidationError` (400): the organization agent limit is full (`Customer has exceeded number of coworkers`). See [the limit](/docs/administration/usage-quota-and-billing#limits-and-behavior).
- `ConflictError` (409): `mode` is `create` and the name is taken, or `create_if_absent` or `upsert` finds several process flows with the name.

**Show full response**

A real response, captured from a live call to `create-process-flow`:

**Result of create-process-flow**

**REST API**

```json
{
  "lifecycle_id": "qlc_000209",
  "result": {
    "flow": {
      "_id": "6ab196817566f41b5c28e33a",
      "agents": [],
      "anomaly_detection": {
        "instructions": "",
        "is_enabled": false
      },
      "created_at": 1790023297.679563,
      "cron_details": {
        "agentflow_id": "",
        "collection_name": null,
        "count": null,
        "executed_by": null,
        "filters": null,
        "frequency": "",
        "goal": {
          "display_template": "",
          "template": ""
        },
        "limit": null,
        "projectID": "",
        "query": {},
        "query_limit": null
      },
      "default_goal": "",
      "description": "Created to capture delete-process-flow, then deleted.",
      "failover_agentflow_ids": null,
      "image": "https://files.example.com/connectors/knowledge_base.jpeg",
      "isActive": true,
      "is_cron_scheduled": false,
      "is_deleted": false,
      "kind": "flow",
      "last_used": 1790023297.679572,
      "name": "docs-fixture-scratch-flow-1790023297678",
      "organization_id": "6a79a770b6c128ccc24bfca8",
      "projectID": "6aa9beb7ec8121a43b6c791b",
      "role": " ",
      "type": "master",
      "unique_id": "1cuikfo",
      "updated_at": 1790023297.679571
    },
    "process_flow_id": "6ab196817566f41b5c28e33a"
  }
}
```

**TypeScript SDK**

```ts
// the object you get back IS the result — there is no `.result` to read
{
  flow: {
    _id: '6ab196817566f41b5c28e33a',
    agents: [],
    anomaly_detection: {
      instructions: '',
      is_enabled: false
    },
    created_at: 1790023297.679563,
    cron_details: {
      agentflow_id: '',
      collection_name: null,
      count: null,
      executed_by: null,
      filters: null,
      frequency: '',
      goal: {
        display_template: '',
        template: ''
      },
      limit: null,
      projectID: '',
      query: {},
      query_limit: null
    },
    default_goal: '',
    description: 'Created to capture delete-process-flow, then deleted.',
    failover_agentflow_ids: null,
    image: 'https://files.example.com/connectors/knowledge_base.jpeg',
    isActive: true,
    is_cron_scheduled: false,
    is_deleted: false,
    kind: 'flow',
    last_used: 1790023297.679572,
    name: 'docs-fixture-scratch-flow-1790023297678',
    organization_id: '6a79a770b6c128ccc24bfca8',
    projectID: '6aa9beb7ec8121a43b6c791b',
    role: ' ',
    type: 'master',
    unique_id: '1cuikfo',
    updated_at: 1790023297.679571
  },
  process_flow_id: '6ab196817566f41b5c28e33a'
}

// The TS SDK names these camelCase — lifecycleId, traceId — where Python uses
// lifecycle_id and trace_id. Both are non-enumerable: readable, but omitted by
// Object.keys() and JSON.stringify(). `result.lifecycleId` here is undefined.
result.lifecycleId // 'qlc_000209'
```

**Python SDK**

```python
# a dict subclass; `result` is already unwrapped
{
 "flow": {
  "_id": "6ab196817566f41b5c28e33a",
  "agents": [],
  "anomaly_detection": {
   "instructions": "",
   "is_enabled": False
  },
  "created_at": 1790023297.679563,
  "cron_details": {
   "agentflow_id": "",
   "collection_name": None,
   "count": None,
   "executed_by": None,
   "filters": None,
   "frequency": "",
   "goal": {
    "display_template": "",
    "template": ""
   },
   "limit": None,
   "projectID": "",
   "query": {},
   "query_limit": None
  },
  "default_goal": "",
  "description": "Created to capture delete-process-flow, then deleted.",
  "failover_agentflow_ids": None,
  "image": "https://files.example.com/connectors/knowledge_base.jpeg",
  "isActive": True,
  "is_cron_scheduled": False,
  "is_deleted": False,
  "kind": "flow",
  "last_used": 1790023297.679572,
  "name": "docs-fixture-scratch-flow-1790023297678",
  "organization_id": "6a79a770b6c128ccc24bfca8",
  "projectID": "6aa9beb7ec8121a43b6c791b",
  "role": " ",
  "type": "master",
  "unique_id": "1cuikfo",
  "updated_at": 1790023297.679571
 },
 "process_flow_id": "6ab196817566f41b5c28e33a"
}

out.lifecycle_id  # 'qlc_000209' — an attribute, not a key
```

**More details**

Two modes. **Shell-only**: pass just `name`, `description` and `role` to create an empty process flow, and add agents later, one at a time, with [`update-process-flow`](/docs/automation/update-process-flow). **Full manifest**: pass a `manifest` (Process flow Manifest v1.0: process flow, `agents[]` of `skilled_agent` and `decider_agent` nodes, wiring) to provision the process flow, all its agents and the wiring in one call. Prefer the manifest form whenever you already know the complete structure, for example after reasoning about what the request needs, or after running a compiler-style process flow and reading its raw JSON through `get-execution`'s `last_agent_output` (never its conclusion, which is always prose). The `docs` tool with `{topic: 'orchestrate'}` returns the full manifest contract and a worked example.

Refusals in full: an invalid manifest (a missing `flow.name`, a duplicate `ref`, an `initial_agent` that names no agent, a `next` pointing at nothing, an unknown `type`; several near-misses carry a hint naming the field you meant, and every problem is reported at once); `agents[].skills` ids that do not exist in this project (an id that does not resolve silently does nothing at run time, so it is rejected up front); a skilled agent with no skills, which the executor cannot run (the conversational form, `role: "conversational"` with `has_knowledge_base: true` and a non-empty `context` array of tag tokens, is the alternative; a prose `context` is refused); and, in shell mode, a missing `name`. The organization agent limit is raised upstream.

`kb_tags` is required when the manifest leaves `knowledge_base.tags` empty and an ingest entry has no tags of its own, and each entry must be a tag token (e.g. `"b2b_saas_icp"`): retrieval matches tags by exact string, so a prose context is refused. `create_if_absent` is safe to repeat. `upsert` fails with 409 when several process flows share the name.

If any agent needs a skill (a connector action), call [`list-skills`](/docs/connectors/list-skills) first to find its real id: this tool never creates skills, only references existing ones. Skills arrive with a provisioned connector; no tool creates one. Provision the connector under **Connectors** in the console, or with [`provision-connector`](/docs/connectors/provision-connector) for a connector that needs no credential: one that needs an API key or an OAuth sign-in still needs a person in the console. If the skill you want is not in `list-skills`, its connector is not provisioned in this project yet. The console's **Skills** screen lists them with name, status, version and which agents use them. Granting a skill means editing an agent, which needs `fabric:orchestrate.write`. You grant skills, not actions: the action is what runs when the agent uses the skill.

`kb_tags` also sets the tags of the manifest's ingest entries. A process flow is a multi-agent orchestration, not a larger agent. It has no instructions, tools or model of its own: the reasoning happens in the agents it names. Reach for one when the work needs more than one worker; one worker's job, however long, is one [agent](/docs/agents/overview). The console calls the screen **Multi-Agent Process flows**: open **Orchestrate**, then **Multi-Agent Process flows**, and choose **Create Multi-Agent Process flow**. The process flow and the agents it names have their own histories, promoted and rolled back independently; see [Executions and versioning](/docs/agents/agent-versions).

### The manifest

The manifest is the definition. Editing it does not edit the agents' instructions, and editing an agent does not rewire the manifest.

| Part | What it holds |
| --- | --- |
| **`manifest_version`** | `"1.0"`, the only value accepted today. Omitting it means the same thing. |
| **`flow`** | The process flow itself: a `name` (required), and optionally a description and a role. |
| **`agents`** | The agents taking part. At least one is required, each with a `ref` (a short label unique within the manifest) and a `title`. |
| **`wiring`** | One field, `initial_agent`, naming the agent that runs first. |

The rest of the manifest points at an agent by its `ref`, including `wiring.initial_agent`, so you need no real ids beforehand. A `decider_agent` needs at least two branches. Validation reports every problem at once. Optional fields are off unless you set them: whether the agent reads the [knowledge base](/docs/context/knowledge-base), whether it pauses for a person before it proceeds, and whether its prompt receives a summary of earlier agents, named by `ref` and resolved to real ids for you.

**Create a process flow from a manifest**

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
flow = fabric.create_process_flow(
    manifest={
        "manifest_version": "1.0",
        "flow": {"name": "orders-triage"},
        "agents": [
            {"ref": "check", "title": "Check order", "prompt": {"template": "Check the order."},
             "skills": ["<skill id from list-skills>"], "next": "notify"},
            {"ref": "notify", "title": "Notify customer", "prompt": {"template": "Tell the customer."},
             "skills": ["<skill id from list-skills>"]},
        ],
        "wiring": {"initial_agent": "check"},
    },
)
```

**TypeScript SDK**

```ts
import { Fabric } from '@wexa-fabric/sdk'

const fabric = new Fabric()
const result = await fabric.createProcessFlow({
  manifest: {
    manifest_version: "1.0",
    flow: { name: "orders-triage" },
    agents: [
      { ref: "check", title: "Check order", prompt: { template: "Check the order." },
        skills: ["<skill id from list-skills>"], next: "notify" },
      { ref: "notify", title: "Notify customer", prompt: { template: "Tell the customer." },
        skills: ["<skill id from list-skills>"] }
    ],
    wiring: { initial_agent: "check" }
  }
})
```

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/create-process-flow \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"manifest":{"manifest_version":"1.0","flow":{"name":"orders-triage"},"agents":[{"ref":"check","title":"Check order","prompt":{"template":"Check the order."},"skills":["<skill id from list-skills>"],"next":"notify"},{"ref":"notify","title":"Notify customer","prompt":{"template":"Tell the customer."},"skills":["<skill id from list-skills>"]}],"wiring":{"initial_agent":"check"}}}'
```

### The three node types

Every entry in `manifest.agents[]` is one node in the process flow. `type` picks which kind, and
**it defaults to `skilled_agent` when you leave it out**. There are three, and no others: an
unknown `type` is refused with a message naming all three.

| **`type`** | What the node does | You must set | Refused on this type |
| --- | --- | --- | --- |
| **`skilled_agent`** | Runs the task itself, inside Wexa, using the skills you give it. The default. | `skills` — **or** make it conversational: `role: "conversational"` with `has_knowledge_base: true` and a non-empty `context` | `registration_id` |
| **`decider_agent`** | Branches. It picks one route and the process flow carries on down it. | `conditions`, at least 2, one per branch; `next_agents` maps each decision to another agent's `ref` | `registration_id`, `is_terminal_agent`, `is_user_specific_task` |
| **`interop_agent`** | The external agent node. Hands its step to an agent on another platform and waits for it. | `registration_id` — a peer agent connection from [`list-peer-agents`](/docs/agents/list-peer-agents) | `skills`, `is_preview_mode_enabled`, `conditions`, `is_terminal_agent`, `is_user_specific_task` |

A skilled agent with no skills is refused, because the executor cannot run it. The refusal says so
and offers the conversational form as the alternative — `context` is an ARRAY of tag tokens like
`["b2b_saas_icp"]`, never a sentence.

### Settings every node type takes

These are the same agent fields create-agent takes, under the manifest's names; the two are mapped
in one table at [agent fields](/docs/agents/create-agent#agent-fields).

| Field | Type | Leave it out and… |
| --- | --- | --- |
| **`title`** | string | Required. It is how the node is named everywhere a person looks. |
| `role`, `role_description` | string | Empty. |
| **`prompt`** | object | `{"template": "..."}` is the task. `display_template` is derived from `template` when you omit it. |
| **`llm`** | object | `model` is an id from list-models `settable`; omitted, the agent is stored with your organization's default registry model, or the first registry model when none is marked default. `max_tokens` 4000; `temperature` 0. |
| `has_knowledge_base` + `context` | boolean + array | Off. `context` holds tag tokens, matched against document tags by exact string. |
| **`context_access`** | object | **Full access.** Send `{"mode": "off"}` to withhold the context graph, or `"brief_only"` for the project brief alone. `mode` is required whenever you send the object at all. |
| **`agent_base_prompt`** | string | The platform default reasoning prompt. |
| `context_required` + `context_target_agent_ids` | boolean + array | Off. When on, name the earlier agents by their `ref`. |

### Settings only a skilled agent takes

| Field | Type | Leave it out and… |
| --- | --- | --- |
| **`is_preview_mode_enabled`** | boolean | Automatic approval. `true` pauses the node for a person to approve. |
| **`is_terminal_agent`** | boolean | Off. Marks the node as the end of the flow. It is recorded and read back, but nothing in the run engine acts on it yet. |
| **`is_user_specific_task`** | boolean | Off. `true` runs the task once per active user rather than once for the whole flow. |

### An external agent node, end to end

Register the external agent first, then point a node at the connection it returns:

```python
conn = fabric.register_peer_agent(
    name="invoice-reconciler",
    endpoint="https://agents.example.com/a2a/invoice-reconciler",
    protocol="a2a",                 # or "mcp"
    secret="<the external agent's token>",
)

fabric.create_process_flow(manifest={
    "manifest_version": "1.0",
    "flow": {"name": "Invoice reconciliation", "role": "conversational"},
    "agents": [{
        "ref": "reconcile",
        "type": "interop_agent",
        "title": "Reconcile with the finance platform",
        "prompt": {"template": "Reconcile the outstanding supplier invoices."},
        "registration_id": conn["connection_id"],
        "next": None,
    }],
    "wiring": {"initial_agent": "reconcile"},
})
```

The node carries no skills and no approval step: the external agent brings its own abilities and
runs its step on its own platform. `has_knowledge_base` and `context_access` still work, and mean
something narrower here — what Wexa knows is gathered once and bundled into what is handed over,
never live access back into your project. Poll the run with
[`get-execution`](/docs/agents/get-execution): a node like this can stay open for a long time, and
it can stop to ask a question.

What the external agent receives is the node prompt, plus the context sources that are on for the
node: its `role_description`, the output of the agents before it, the project brief and the
knowledge base. Set `context_access` to `{"mode": "off"}` for an external agent you do not trust,
so that project information does not go to a third party.

An MCP external agent must expose exactly one tool, or the handshake fails and so does the node
when it runs. Check the endpoint with [`handshake-peer-agent`](/docs/agents/handshake-peer-agent)
before you build the process flow.

A question from the external agent waits for a person to answer it in the console. While it waits,
the execution has status `interop_pending` and carries the question — see
[a run waiting on an external agent](/docs/agents/get-execution#a-run-waiting-on-an-external-agent).
An MCP question is cancelled after 3 minutes with no answer. An A2A question has no time limit, and
no tool cancels a parked run, so plan for a person to be there.

The external agent also gets the run goal. A `{{goal}}` placeholder in the node prompt is filled
with the `goal` you give `run-process-flow`, so `Handle case {{goal}}` with a goal of `CASE-123` is
sent as `Handle case CASE-123`. The goal is also sent on its own `Goal:` line, and as `goal` in the
structured context, unless the node prompt already is the goal. A value the node itself takes as an
input named `goal` wins over the run goal.

An external agent returns no token usage. An external step shows only what Wexa spends on its own
side: the knowledge base lookup when it is on, otherwise 0 tokens and $0.00.

Run the process flow with [`run-process-flow`](/docs/automation/run-process-flow) and read it back with [`get-process-flow`](/docs/automation/get-process-flow). Every agent in a process flow makes its own governed calls: a policy decision that refuses one agent's call refuses that call, not the process flow's right to exist. To hand a step to an agent on another platform, see [peer agents](/docs/agents/register-peer-agent).

Marked consequential: it is audited in full, and a policy rule may hold it for a person to approve. See [Policy and approvals](/docs/governance/policy-and-approvals).

Every tool call works the same way: [How every tool call works](/docs/sdks-and-apis/how-every-tool-call-works).
