Save Workflow Draft API
Save the draft definition using an optimistic-concurrency revision token, so concurrent editors never silently overwrite each other.
curl --request PUT \
--url https://api.bolna.ai/workflows/{workflow_id}/draft \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"expected_revision": 1,
"definition": {
"entry_node_id": "n_start",
"on_no_match": "n_end_unhandled",
"nodes": [
{
"id": "n_start",
"type": "start",
"config": {
"trigger": {
"kind": "manual"
}
},
"cases": [
{
"when": {
"always": true
},
"then": {
"to": "n_call1"
}
}
]
},
{
"id": "n_call1",
"type": "agent",
"name": "Qualify",
"config": {
"agent_id": "123e4567-e89b-12d3-a456-426655440000",
"timeout_s": 3600
},
"cases": [
{
"when": {
"cmp": "==",
"left": {
"var": "call.status"
},
"right": {
"const": "completed"
}
},
"then": {
"to": "n_end_done"
}
}
]
},
{
"id": "n_end_done",
"type": "end",
"config": {
"label": "reached",
"outcome": "success"
}
},
{
"id": "n_end_unhandled",
"type": "end",
"config": {
"label": "unhandled",
"outcome": "failure"
}
}
]
}
}
'import requests
url = "https://api.bolna.ai/workflows/{workflow_id}/draft"
payload = {
"expected_revision": 1,
"definition": {
"entry_node_id": "n_start",
"on_no_match": "n_end_unhandled",
"nodes": [
{
"id": "n_start",
"type": "start",
"config": { "trigger": { "kind": "manual" } },
"cases": [
{
"when": { "always": True },
"then": { "to": "n_call1" }
}
]
},
{
"id": "n_call1",
"type": "agent",
"name": "Qualify",
"config": {
"agent_id": "123e4567-e89b-12d3-a456-426655440000",
"timeout_s": 3600
},
"cases": [
{
"when": {
"cmp": "==",
"left": { "var": "call.status" },
"right": { "const": "completed" }
},
"then": { "to": "n_end_done" }
}
]
},
{
"id": "n_end_done",
"type": "end",
"config": {
"label": "reached",
"outcome": "success"
}
},
{
"id": "n_end_unhandled",
"type": "end",
"config": {
"label": "unhandled",
"outcome": "failure"
}
}
]
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
expected_revision: 1,
definition: {
entry_node_id: 'n_start',
on_no_match: 'n_end_unhandled',
nodes: [
{
id: 'n_start',
type: 'start',
config: {trigger: {kind: 'manual'}},
cases: [{when: {always: true}, then: {to: 'n_call1'}}]
},
{
id: 'n_call1',
type: 'agent',
name: 'Qualify',
config: {agent_id: '123e4567-e89b-12d3-a456-426655440000', timeout_s: 3600},
cases: [
{
when: {cmp: '==', left: {var: 'call.status'}, right: {const: 'completed'}},
then: {to: 'n_end_done'}
}
]
},
{id: 'n_end_done', type: 'end', config: {label: 'reached', outcome: 'success'}},
{
id: 'n_end_unhandled',
type: 'end',
config: {label: 'unhandled', outcome: 'failure'}
}
]
}
})
};
fetch('https://api.bolna.ai/workflows/{workflow_id}/draft', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.bolna.ai/workflows/{workflow_id}/draft",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'expected_revision' => 1,
'definition' => [
'entry_node_id' => 'n_start',
'on_no_match' => 'n_end_unhandled',
'nodes' => [
[
'id' => 'n_start',
'type' => 'start',
'config' => [
'trigger' => [
'kind' => 'manual'
]
],
'cases' => [
[
'when' => [
'always' => true
],
'then' => [
'to' => 'n_call1'
]
]
]
],
[
'id' => 'n_call1',
'type' => 'agent',
'name' => 'Qualify',
'config' => [
'agent_id' => '123e4567-e89b-12d3-a456-426655440000',
'timeout_s' => 3600
],
'cases' => [
[
'when' => [
'cmp' => '==',
'left' => [
'var' => 'call.status'
],
'right' => [
'const' => 'completed'
]
],
'then' => [
'to' => 'n_end_done'
]
]
]
],
[
'id' => 'n_end_done',
'type' => 'end',
'config' => [
'label' => 'reached',
'outcome' => 'success'
]
],
[
'id' => 'n_end_unhandled',
'type' => 'end',
'config' => [
'label' => 'unhandled',
'outcome' => 'failure'
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.bolna.ai/workflows/{workflow_id}/draft"
payload := strings.NewReader("{\n \"expected_revision\": 1,\n \"definition\": {\n \"entry_node_id\": \"n_start\",\n \"on_no_match\": \"n_end_unhandled\",\n \"nodes\": [\n {\n \"id\": \"n_start\",\n \"type\": \"start\",\n \"config\": {\n \"trigger\": {\n \"kind\": \"manual\"\n }\n },\n \"cases\": [\n {\n \"when\": {\n \"always\": true\n },\n \"then\": {\n \"to\": \"n_call1\"\n }\n }\n ]\n },\n {\n \"id\": \"n_call1\",\n \"type\": \"agent\",\n \"name\": \"Qualify\",\n \"config\": {\n \"agent_id\": \"123e4567-e89b-12d3-a456-426655440000\",\n \"timeout_s\": 3600\n },\n \"cases\": [\n {\n \"when\": {\n \"cmp\": \"==\",\n \"left\": {\n \"var\": \"call.status\"\n },\n \"right\": {\n \"const\": \"completed\"\n }\n },\n \"then\": {\n \"to\": \"n_end_done\"\n }\n }\n ]\n },\n {\n \"id\": \"n_end_done\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"reached\",\n \"outcome\": \"success\"\n }\n },\n {\n \"id\": \"n_end_unhandled\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"unhandled\",\n \"outcome\": \"failure\"\n }\n }\n ]\n }\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.bolna.ai/workflows/{workflow_id}/draft")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"expected_revision\": 1,\n \"definition\": {\n \"entry_node_id\": \"n_start\",\n \"on_no_match\": \"n_end_unhandled\",\n \"nodes\": [\n {\n \"id\": \"n_start\",\n \"type\": \"start\",\n \"config\": {\n \"trigger\": {\n \"kind\": \"manual\"\n }\n },\n \"cases\": [\n {\n \"when\": {\n \"always\": true\n },\n \"then\": {\n \"to\": \"n_call1\"\n }\n }\n ]\n },\n {\n \"id\": \"n_call1\",\n \"type\": \"agent\",\n \"name\": \"Qualify\",\n \"config\": {\n \"agent_id\": \"123e4567-e89b-12d3-a456-426655440000\",\n \"timeout_s\": 3600\n },\n \"cases\": [\n {\n \"when\": {\n \"cmp\": \"==\",\n \"left\": {\n \"var\": \"call.status\"\n },\n \"right\": {\n \"const\": \"completed\"\n }\n },\n \"then\": {\n \"to\": \"n_end_done\"\n }\n }\n ]\n },\n {\n \"id\": \"n_end_done\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"reached\",\n \"outcome\": \"success\"\n }\n },\n {\n \"id\": \"n_end_unhandled\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"unhandled\",\n \"outcome\": \"failure\"\n }\n }\n ]\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.bolna.ai/workflows/{workflow_id}/draft")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"expected_revision\": 1,\n \"definition\": {\n \"entry_node_id\": \"n_start\",\n \"on_no_match\": \"n_end_unhandled\",\n \"nodes\": [\n {\n \"id\": \"n_start\",\n \"type\": \"start\",\n \"config\": {\n \"trigger\": {\n \"kind\": \"manual\"\n }\n },\n \"cases\": [\n {\n \"when\": {\n \"always\": true\n },\n \"then\": {\n \"to\": \"n_call1\"\n }\n }\n ]\n },\n {\n \"id\": \"n_call1\",\n \"type\": \"agent\",\n \"name\": \"Qualify\",\n \"config\": {\n \"agent_id\": \"123e4567-e89b-12d3-a456-426655440000\",\n \"timeout_s\": 3600\n },\n \"cases\": [\n {\n \"when\": {\n \"cmp\": \"==\",\n \"left\": {\n \"var\": \"call.status\"\n },\n \"right\": {\n \"const\": \"completed\"\n }\n },\n \"then\": {\n \"to\": \"n_end_done\"\n }\n }\n ]\n },\n {\n \"id\": \"n_end_done\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"reached\",\n \"outcome\": \"success\"\n }\n },\n {\n \"id\": \"n_end_unhandled\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"unhandled\",\n \"outcome\": \"failure\"\n }\n }\n ]\n }\n}"
response = http.request(request)
puts response.read_body{
"revision": 123
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}expected_revision is a compare-and-set token. A fresh workflow’s draft is at revision 0, each save returns the next revision, and publishing resets the new draft to 0. If your value is stale the save fails with 409 revision_conflict carrying current_revision — re-read the draft and retry.Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
The unique id of the workflow
Body
The revision you last read. A fresh workflow's draft starts at 0, and publish resets the new draft to 0. A stale value returns 409 revision_conflict carrying current_revision.
x >= 0The workflow graph. nodes is a flat list of node envelopes — {id, type, name, config, cases} — where type is one of start, agent, extraction, api, time, retry, aisensy_whatsapp or end. Each node's cases array routes to the next node via condition expressions (time nodes use a single to instead, end nodes have neither). See Nodes and Conditions and variables for the full schema, and the Node Types API for every config parameter with defaults and bounds.
Show child attributes
Show child attributes
{
"entry_node_id": "n_start",
"on_no_match": "n_end_unhandled",
"nodes": [
{
"id": "n_start",
"type": "start",
"config": { "trigger": { "kind": "manual" } },
"cases": [
{
"when": { "always": true },
"then": { "to": "n_call1" }
}
]
},
{
"id": "n_call1",
"type": "agent",
"name": "Qualify",
"config": {
"agent_id": "123e4567-e89b-12d3-a456-426655440000",
"timeout_s": 3600
},
"cases": [
{
"when": {
"cmp": "==",
"left": { "var": "call.status" },
"right": { "const": "completed" }
},
"then": { "to": "n_end_done" }
}
]
},
{
"id": "n_end_done",
"type": "end",
"config": { "label": "reached", "outcome": "success" }
},
{
"id": "n_end_unhandled",
"type": "end",
"config": {
"label": "unhandled",
"outcome": "failure"
}
}
]
}
Response
Draft saved
The new draft revision. Send it as expected_revision on the next save.
Was this page helpful?
curl --request PUT \
--url https://api.bolna.ai/workflows/{workflow_id}/draft \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"expected_revision": 1,
"definition": {
"entry_node_id": "n_start",
"on_no_match": "n_end_unhandled",
"nodes": [
{
"id": "n_start",
"type": "start",
"config": {
"trigger": {
"kind": "manual"
}
},
"cases": [
{
"when": {
"always": true
},
"then": {
"to": "n_call1"
}
}
]
},
{
"id": "n_call1",
"type": "agent",
"name": "Qualify",
"config": {
"agent_id": "123e4567-e89b-12d3-a456-426655440000",
"timeout_s": 3600
},
"cases": [
{
"when": {
"cmp": "==",
"left": {
"var": "call.status"
},
"right": {
"const": "completed"
}
},
"then": {
"to": "n_end_done"
}
}
]
},
{
"id": "n_end_done",
"type": "end",
"config": {
"label": "reached",
"outcome": "success"
}
},
{
"id": "n_end_unhandled",
"type": "end",
"config": {
"label": "unhandled",
"outcome": "failure"
}
}
]
}
}
'import requests
url = "https://api.bolna.ai/workflows/{workflow_id}/draft"
payload = {
"expected_revision": 1,
"definition": {
"entry_node_id": "n_start",
"on_no_match": "n_end_unhandled",
"nodes": [
{
"id": "n_start",
"type": "start",
"config": { "trigger": { "kind": "manual" } },
"cases": [
{
"when": { "always": True },
"then": { "to": "n_call1" }
}
]
},
{
"id": "n_call1",
"type": "agent",
"name": "Qualify",
"config": {
"agent_id": "123e4567-e89b-12d3-a456-426655440000",
"timeout_s": 3600
},
"cases": [
{
"when": {
"cmp": "==",
"left": { "var": "call.status" },
"right": { "const": "completed" }
},
"then": { "to": "n_end_done" }
}
]
},
{
"id": "n_end_done",
"type": "end",
"config": {
"label": "reached",
"outcome": "success"
}
},
{
"id": "n_end_unhandled",
"type": "end",
"config": {
"label": "unhandled",
"outcome": "failure"
}
}
]
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
expected_revision: 1,
definition: {
entry_node_id: 'n_start',
on_no_match: 'n_end_unhandled',
nodes: [
{
id: 'n_start',
type: 'start',
config: {trigger: {kind: 'manual'}},
cases: [{when: {always: true}, then: {to: 'n_call1'}}]
},
{
id: 'n_call1',
type: 'agent',
name: 'Qualify',
config: {agent_id: '123e4567-e89b-12d3-a456-426655440000', timeout_s: 3600},
cases: [
{
when: {cmp: '==', left: {var: 'call.status'}, right: {const: 'completed'}},
then: {to: 'n_end_done'}
}
]
},
{id: 'n_end_done', type: 'end', config: {label: 'reached', outcome: 'success'}},
{
id: 'n_end_unhandled',
type: 'end',
config: {label: 'unhandled', outcome: 'failure'}
}
]
}
})
};
fetch('https://api.bolna.ai/workflows/{workflow_id}/draft', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.bolna.ai/workflows/{workflow_id}/draft",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'expected_revision' => 1,
'definition' => [
'entry_node_id' => 'n_start',
'on_no_match' => 'n_end_unhandled',
'nodes' => [
[
'id' => 'n_start',
'type' => 'start',
'config' => [
'trigger' => [
'kind' => 'manual'
]
],
'cases' => [
[
'when' => [
'always' => true
],
'then' => [
'to' => 'n_call1'
]
]
]
],
[
'id' => 'n_call1',
'type' => 'agent',
'name' => 'Qualify',
'config' => [
'agent_id' => '123e4567-e89b-12d3-a456-426655440000',
'timeout_s' => 3600
],
'cases' => [
[
'when' => [
'cmp' => '==',
'left' => [
'var' => 'call.status'
],
'right' => [
'const' => 'completed'
]
],
'then' => [
'to' => 'n_end_done'
]
]
]
],
[
'id' => 'n_end_done',
'type' => 'end',
'config' => [
'label' => 'reached',
'outcome' => 'success'
]
],
[
'id' => 'n_end_unhandled',
'type' => 'end',
'config' => [
'label' => 'unhandled',
'outcome' => 'failure'
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.bolna.ai/workflows/{workflow_id}/draft"
payload := strings.NewReader("{\n \"expected_revision\": 1,\n \"definition\": {\n \"entry_node_id\": \"n_start\",\n \"on_no_match\": \"n_end_unhandled\",\n \"nodes\": [\n {\n \"id\": \"n_start\",\n \"type\": \"start\",\n \"config\": {\n \"trigger\": {\n \"kind\": \"manual\"\n }\n },\n \"cases\": [\n {\n \"when\": {\n \"always\": true\n },\n \"then\": {\n \"to\": \"n_call1\"\n }\n }\n ]\n },\n {\n \"id\": \"n_call1\",\n \"type\": \"agent\",\n \"name\": \"Qualify\",\n \"config\": {\n \"agent_id\": \"123e4567-e89b-12d3-a456-426655440000\",\n \"timeout_s\": 3600\n },\n \"cases\": [\n {\n \"when\": {\n \"cmp\": \"==\",\n \"left\": {\n \"var\": \"call.status\"\n },\n \"right\": {\n \"const\": \"completed\"\n }\n },\n \"then\": {\n \"to\": \"n_end_done\"\n }\n }\n ]\n },\n {\n \"id\": \"n_end_done\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"reached\",\n \"outcome\": \"success\"\n }\n },\n {\n \"id\": \"n_end_unhandled\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"unhandled\",\n \"outcome\": \"failure\"\n }\n }\n ]\n }\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.bolna.ai/workflows/{workflow_id}/draft")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"expected_revision\": 1,\n \"definition\": {\n \"entry_node_id\": \"n_start\",\n \"on_no_match\": \"n_end_unhandled\",\n \"nodes\": [\n {\n \"id\": \"n_start\",\n \"type\": \"start\",\n \"config\": {\n \"trigger\": {\n \"kind\": \"manual\"\n }\n },\n \"cases\": [\n {\n \"when\": {\n \"always\": true\n },\n \"then\": {\n \"to\": \"n_call1\"\n }\n }\n ]\n },\n {\n \"id\": \"n_call1\",\n \"type\": \"agent\",\n \"name\": \"Qualify\",\n \"config\": {\n \"agent_id\": \"123e4567-e89b-12d3-a456-426655440000\",\n \"timeout_s\": 3600\n },\n \"cases\": [\n {\n \"when\": {\n \"cmp\": \"==\",\n \"left\": {\n \"var\": \"call.status\"\n },\n \"right\": {\n \"const\": \"completed\"\n }\n },\n \"then\": {\n \"to\": \"n_end_done\"\n }\n }\n ]\n },\n {\n \"id\": \"n_end_done\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"reached\",\n \"outcome\": \"success\"\n }\n },\n {\n \"id\": \"n_end_unhandled\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"unhandled\",\n \"outcome\": \"failure\"\n }\n }\n ]\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.bolna.ai/workflows/{workflow_id}/draft")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"expected_revision\": 1,\n \"definition\": {\n \"entry_node_id\": \"n_start\",\n \"on_no_match\": \"n_end_unhandled\",\n \"nodes\": [\n {\n \"id\": \"n_start\",\n \"type\": \"start\",\n \"config\": {\n \"trigger\": {\n \"kind\": \"manual\"\n }\n },\n \"cases\": [\n {\n \"when\": {\n \"always\": true\n },\n \"then\": {\n \"to\": \"n_call1\"\n }\n }\n ]\n },\n {\n \"id\": \"n_call1\",\n \"type\": \"agent\",\n \"name\": \"Qualify\",\n \"config\": {\n \"agent_id\": \"123e4567-e89b-12d3-a456-426655440000\",\n \"timeout_s\": 3600\n },\n \"cases\": [\n {\n \"when\": {\n \"cmp\": \"==\",\n \"left\": {\n \"var\": \"call.status\"\n },\n \"right\": {\n \"const\": \"completed\"\n }\n },\n \"then\": {\n \"to\": \"n_end_done\"\n }\n }\n ]\n },\n {\n \"id\": \"n_end_done\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"reached\",\n \"outcome\": \"success\"\n }\n },\n {\n \"id\": \"n_end_unhandled\",\n \"type\": \"end\",\n \"config\": {\n \"label\": \"unhandled\",\n \"outcome\": \"failure\"\n }\n }\n ]\n }\n}"
response = http.request(request)
puts response.read_body{
"revision": 123
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}{
"detail": {
"code": "revision_conflict",
"message": "<string>"
}
}
