Update Workflow Definition
PUT/workflows/:workflowId/definition
Replace a workflow definition in full. This is a complete overwrite — any step absent from the request is deleted, so always read the current definition first and send it back modified — the definition carries its own version, so sending back what you read keeps you protected against overwriting a concurrent edit. A stale version is rejected with 422 and nothing is written.
Draft only. A published workflow is rejected with 409: the overwrite would replace steps under contacts currently moving through it. Set the workflow to draft with Update Workflow Status, update it, then publish it again.
Order matters. Changing status advances the version, so read the definition after setting the workflow to draft: draft → read → update → publish. Reading first and drafting second leaves you holding a stale version.
Request
API Version
v3Workflow ID
Sub-account (location) ID the workflow belongs to
- application/json
- Body
- linear
- branching
- wait
- webhook
Bodyrequired
Workflow name
Where this definition came from
Triggers that enrol contacts. May be empty — a workflow with no trigger can still be entered by another workflow or by the API — but the key must be present.
How multiple triggers combine. Echoed by Get Workflow Definition; ignored on write — the system always stores or.
andorSteps. May be an empty array, but the key must be present.
Connections between steps. May be empty, but the key must be present.
Workflow-level settings
Single-segment folder to place the workflow in. Mutually exclusive with folderPath.
Folder path, outermost first, max 10 levels. Mutually exclusive with folderName.
Create the folder if it does not exist. Requires folderName or folderPath.
The workflow's version counter, owned by the system — not a format version. Get Workflow Definition fills it in; leave it untouched and send it back on update, and a concurrent edit in the builder can never be silently overwritten. A stale value is rejected with 422.
Treat it as opaque: echo it, never compute from it. It does not always advance by one (delete and restore each add 0.1, and a later save rounds up), so it is not always an integer.
Required on update: send back the value you read. An update without it is rejected with 422 before anything is written.
- Linear — two steps in sequence
{
"version": 1,
"name": "Welcome new contacts",
"source": {
"system": "api"
},
"triggers": [
{
"type": "contact_created",
"name": "New contact"
}
],
"actions": [
{
"id": "send-welcome-sms",
"type": "sms",
"name": "Send welcome SMS",
"attributes": {
"body": "Hi {{contact.first_name}}, thanks for getting in touch!"
}
},
{
"id": "tag-as-welcomed",
"type": "add_contact_tag",
"name": "Tag as welcomed",
"attributes": {
"tags": [
"your-existing-tag"
]
}
}
],
"edges": [
{
"from": "send-welcome-sms",
"to": "tag-as-welcomed"
}
]
}
- If/else — two lanes from one condition
{
"version": 1,
"name": "Route by tag",
"source": {
"system": "api"
},
"triggers": [
{
"type": "contact_created"
}
],
"actions": [
{
"id": "is-vip",
"type": "if_else",
"name": "Is this a VIP?",
"attributes": {
"conditionName": "Is this a VIP?",
"operator": "and",
"if": true
}
},
{
"id": "vip-greeting",
"type": "sms",
"name": "VIP greeting",
"attributes": {
"body": "Welcome back!"
}
},
{
"id": "standard-greeting",
"type": "sms",
"name": "Standard greeting",
"attributes": {
"body": "Welcome!"
}
}
],
"edges": [
{
"from": "is-vip",
"to": "vip-greeting",
"branch": "yes"
},
{
"from": "is-vip",
"to": "standard-greeting",
"branch": "default"
}
]
}
- Wait — pause before the next step
{
"version": 1,
"name": "Follow up after two days",
"source": {
"system": "api"
},
"triggers": [
{
"type": "contact_created"
}
],
"actions": [
{
"id": "wait-one-day",
"type": "wait",
"name": "Wait 2 days",
"attributes": {
"type": "time",
"startAfter": {
"when": "after",
"type": "days",
"value": 2
}
}
},
{
"id": "send-welcome-sms",
"type": "sms",
"name": "Follow-up SMS",
"attributes": {
"body": "Still interested?"
}
}
],
"edges": [
{
"from": "wait-one-day",
"to": "send-welcome-sms"
}
],
"settings": {
"timezone": "account",
"window": {
"condition": "when",
"start": "09:00",
"end": "17:00",
"days": [
1,
2,
3,
4,
5
]
}
}
}
- Webhook — call an external system
{
"version": 1,
"name": "Notify external CRM",
"source": {
"system": "api"
},
"triggers": [
{
"type": "contact_created"
}
],
"actions": [
{
"id": "send-welcome-sms",
"type": "webhook",
"name": "POST to CRM",
"attributes": {
"method": "POST",
"url": "https://example.com/hooks/new-contact",
"headers": [
{
"key": "X-Api-Key",
"value": "your-api-key"
}
],
"customData": [
{
"key": "email",
"value": "{{contact.email}}"
},
{
"key": "source",
"value": "highlevel"
}
]
}
}
],
"edges": []
}
Workflow updated
- application/json
- Schema
- Example (auto)
Schema
Workflow ID
Whether the workflow is live. Read-only here — change it with Update Workflow Status, which is gated by the workflows.publish scope. Absent only in the rare case where the update succeeded but the workflow could not be read back.
draftpublishedThe updated workflow, in the same shape Get Workflow Definition returns and this endpoint accepts — so a follow-up edit needs no separate read. Note version has advanced: an update increments the counter, and sending a stale one is rejected with 422. Absent only if the update succeeded but the workflow could not be read back.
Non-blocking findings
{
"workflowId": "b1b0de82-3efb-4d63-83f6-13c50aef208c",
"status": "draft",
"definition": {
"version": 4,
"name": "Welcome new contacts",
"source": {
"system": "api"
},
"triggers": [],
"actions": [],
"edges": []
},
"warnings": [
{
"path": "actions[4]",
"message": "Step has no incoming edges and may be unreachable."
}
]
}