Build automations
These are the routes behind the app’s automation builder. Every route here needs the manage_campaigns permission unless it says otherwise. Test keys can read, but any change is refused with 403 test_key_not_allowed. To create, change, turn on or off and delete an automation from a plain list of steps, see Automations.
#The flow graph
An automation built on the canvas is saved as a graph: nodes (a trigger node and one node per step, each with an id, a type, its settings and a position), edges ({ "from", "to" } between node ids) and a viewport. Each save turns the graph into the steps the automation runs. The easy way to make a valid graph is to read one with GET /automations/:id/graph and send back a changed copy.
#Get an automation’s graph
The saved graph. An automation that was never saved from the canvas gets a graph laid out from its steps, so every automation has one.
Parameters
idinteger, pathRequired- The automation id.
Returns
data.idinteger- The automation id.
data.namestring- Its name.
data.enabledboolean- Whether it is on.
data.graphobjectnodes,edgesandviewport.
Errors
| HTTP | Code | When |
|---|---|---|
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 500 | GRAPH_UNAVAILABLE | Its steps couldn’t be laid out as a graph. Nothing was changed; contact support. |
#Save an automation’s graph
Replaces the graph and the steps it runs. The graph it replaces is kept as an earlier version. Leave out enabled to keep it on or off as it is.
Parameters
idinteger, pathRequired- The automation id.
graphobjectRequired- The whole graph:
nodes,edgesandviewport. namestring- A new name.
enabledbooleantrueturns it on, any other value turns it off. An automation that is off stays off if the graph isn’t finished;blocked_activationsays why.check_empty_mergeboolean- As on Update an automation: when this save turns it on, ask first about merge fields that are blank for some of your newest contacts (
confirm_empty_merge: trueto go ahead). Also addsempty_mergeto the answer.
Returns
dataobject- The saved Automation object, with its
graph_jsonandtriggers. unreachablearray- Steps the trigger can’t reach. Empty when every step is connected.
waiting_contactsinteger- Contacts waiting in this automation right now.
warningstring- Present when some steps are not connected.
blocked_activationstring- Present when you asked to turn it on and it isn’t finished. It was saved, and it is off.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | GRAPH_REQUIRED | No graph. |
| 400 | GRAPH_INVALID | Part of the graph isn’t set up correctly. Other codes, such as GRAPH_HAS_LOOP, GRAPH_EMPTY, TRIGGER_INVALID or WEBHOOK_URL_INVALID, name the problem; error says what to fix and step_id may name the node. |
| 400 | NAME_REQUIRED | A blank name. |
| 400 | SHEETS_ACTIVATION_FAILED | A Google Sheets trigger couldn’t be set up. |
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 500 | GRAPH_SAVE_FAILED | We couldn’t save. Retry. |
#Create an automation from a graph
Saves a new automation from a graph. It is saved off unless you send enabled: true, and stays off if the graph isn’t finished. Takes an optional Idempotency-Key header: a repeat with the same key returns the first answer.
Parameters
namestringRequired- Up to 180 characters.
graphobjectRequirednodes,edgesandviewport.enabledbooleantrueto save it on.check_empty_mergeboolean- Adds
empty_mergeto the answer. It never stops the save.
Returns
dataobject- The new Automation object, with its
graph_jsonandtriggers.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | NAME_REQUIRED | No name. |
| 400 | GRAPH_REQUIRED | No graph. |
| 400 | GRAPH_INVALID | As on Save an automation’s graph, with the same other codes. |
| 400 | SHEETS_ACTIVATION_FAILED | A Google Sheets trigger couldn’t be set up. |
| 500 | AUTOMATION_CREATE_FAILED | outcome_unknown: true: it may have been saved. List your automations before you retry. |
#Settings
Each automation has its own settings: whether a contact can go through it again, whether a reply stops it, and the hours its texts go out. The number a text goes from is set on each send step.
#Get an automation’s settings
Its saved settings. saved: false means none were ever saved and you are seeing the defaults.
Returns
data.settingsobject- The fields of Update an automation’s settings.
data.savedboolean- Whether these were saved.
Errors
| HTTP | Code | When |
|---|---|---|
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
#Update an automation’s settings
Send only the fields you change; the rest keep their saved value. A field the automation never saved starts at its default.
Parameters
idinteger, pathRequired- The automation id.
settingsobjectRequired- The fields below.
settings.reentryboolean- Let a contact go through it more than once. Default
true. settings.stop_on_replyboolean- End a contact’s run when they reply. Default
false. settings.windowobject | null- The hours its texts go out:
enabled(boolean),tz("account"or"contact"),startandend(minutes after midnight,0to1440;endafterstart) anddays(0is Sunday to6Saturday; at least one whenenabled). Defaultnull: no window of its own.
Returns
dataobject- The saved
settingsandsaved: true.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | SETTINGS_INVALID | A field can’t be used. error says which. |
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 500 | SETTINGS_SAVE_FAILED | Nothing was changed. Retry. |
#Testing
Three ways to check an automation before it goes live, from safest to most real: check the trigger against a sample event, text its messages to your own phone, or run it for real on one contact.
#Check an automation’s trigger
Whether its trigger would start on a sample event. Only the trigger is checked: no condition or step runs, and nothing is sent or saved.
Parameters
idinteger, pathRequired- The automation id.
eventstring- The event name. Leave it out to use the automation’s
trigger_type. dataobject- The sample event’s data, such as
{ "tag": "hot" }for a Contact tagged trigger. A Manual enrollment trigger never matches.
Returns
matchedboolean- Whether the trigger would start.
ruleobject- The automation:
id,name,enabled,trigger_type,trigger_configandactions.
Errors
| HTTP | Code | When |
|---|---|---|
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 500 | TEST_FAILED | Nothing was sent. Retry in a minute. |
#Text a preview to yourself
Texts the automation’s first 5 text steps, from every branch except a Wait for reply path or a Loop, to the account owner’s own phone, each starting with [TEST]. Merge fields are filled from the owner’s name, phone and email. No other step runs, and the texts are free. The check that your phone can get texts may be billed as one number lookup, at the price shown under Your price in Settings > Billing. The owner’s phone must be saved and verified in Settings. Before sending we check that phone: if it is opted out, blocked, or in quiet hours, nothing is sent. Each call texts again; send an Idempotency-Key header to make a retry safe.
Parameters
idinteger, pathRequired- The automation id.
Returns
sentinteger- Texts sent.
ofinteger- Text steps found, up to 5. With none,
sentis0andmessagesays so. tostring- The owner’s phone. A team member gets
recipient: "owner"instead. blockedstring[]- Why each text that didn’t go out was stopped. Only present when one was.
reply_path_textsinteger- Texts under a Wait for reply path that this test left out. Only present when there are some.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | no_phone | The owner has no phone number in Settings. |
| 400 | phone_not_verified | The owner’s phone isn’t verified. |
| 400 | no_active_number | You have no number that can send. |
| 402 | BILLING_PAST_DUE | The account is past due. |
| 403 | PREVIEW_NUMBER_OPTED_OUT | The owner’s phone is opted out of texts from this account. |
| 403 | PREVIEW_QUIET_HOURS | It is quiet hours for the owner’s phone. Try again later. |
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 422 | PREVIEW_NOT_SENT | Every text was stopped. blocked says why. |
| 429 | rate_limited | Too many sends. Wait and retry. |
| 500 | TEST_SEND_FAILED | No texts went out. Retry in a moment. |
#Run an automation on one contact
A real run, now, for one contact, whether the automation is on or off. Every step runs: texts go to the contact and cost your per-segment rate, tags are added, and waits are scheduled. The contact’s last text is used as the reply, and AI intent branches take their yes path. Opt-out still applies to every text. In quiet hours a text waits, as in a normal run; see send_now. Check first with GET /automations/:id/test-run/quiet-check. Takes an optional Idempotency-Key header.
Parameters
idinteger, pathRequired- The automation id.
contact_idintegerRequired- The contact to run it on.
send_nowboolean- Send its texts now even outside your own texting days and hours. It lifts only your own texting days and hours, never the legal window for the contact’s area code (8am to 9pm in most states, 8am to 8pm in Florida and Oklahoma, 9am to 8pm in Connecticut) or a contact whose time zone is unknown; texts outside it still wait. We keep this choice, with your IP address and user agent, as your confirmation that the contact agreed to test texts.
Returns
ranobject[]- Each step that ran:
type,ok, andskipped,failedorerrorwhen it didn’t work. waitedbooleantruewhen the run reached a wait. The steps after it run when the wait ends.stepsinteger- Steps that ran.
failed_stepsinteger- Steps that failed. When any did,
warningtells you not to run it again for this contact: the steps that ran already happened. rule_id,rule_name,contact_id,contact_name- What ran, for whom.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | CONTACT_REQUIRED | No contact_id. |
| 400 | NO_STEPS | It has no steps. |
| 402 | BILLING_PAST_DUE | The account is past due. |
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 404 | CONTACT_NOT_FOUND | No contact with this id on your account. |
| 500 | SEND_NOW_NOT_RECORDED | We couldn’t record your send_now choice, so nothing ran. |
| 500 | TEST_RUN_FAILED | Nothing ran. Retry in a minute. |
#Check quiet hours for a test run
Whether it is quiet hours for a contact right now. Sends nothing.
Parameters
idinteger, pathRequired- The automation id.
contact_idinteger, queryRequired- The contact.
Returns
quietbooleantruein quiet hours.recipient_tzstring | null- The contact’s time zone.
next_allowed_atISO 8601 string | null- When texting is allowed again.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | CONTACT_REQUIRED | No contact_id. |
| 404 | CONTACT_NOT_FOUND | No contact with this id on your account. |
| 500 | QUIET_CHECK_FAILED | Retry in a minute. |
#Test a Filter, Formatter, Math or Loop step
Runs one of these steps on sample values and returns what a real run would compute. No contact, nothing sent or saved.
Parameters
stepobjectRequired- The step, as it is in the automation. Its
typeisfilter,formatter,mathorloop. samplesobject- Sample values, keyed by the name inside each merge field (
webhook.pricefor{{webhook.price}}) or, for a Filter, by each condition’s field. For a Loop, the sample webhook body as a JSON string in__sample_json.
Returns
valueany- Formatter and Math: the result. A Formatter that splits also returns
parts. passedboolean- Filter: whether the sample passes.
count,runs,capped,first_items- Loop: how many items it found, how many it would run, whether that hit the limit, and the first 3 items.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | TOOL_TEST_UNSUPPORTED | The step isn’t one of the four types. |
| 413 | TOOL_SAMPLE_TOO_LARGE | The body is over 200,000 characters. |
| 422 | no code | The step can’t run on this sample. error says why. |
| 500 | TOOL_TEST_FAILED | Retry. |
#Folders
Folders group automations in the app. Put an automation in one with folder_id on Update an automation.
#List folders
Every folder, by name: id, name, parent_id (null at the top level) and count, the automations in it.
#Create a folder
Returns the new folder in data.
Parameters
namestringRequired- Up to 120 characters; a longer name is cut.
parent_idinteger- Put it inside this folder.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | FOLDER_NAME_REQUIRED | No name. |
| 400 | FOLDER_PARENT_NOT_FOUND | No folder with parent_id on your account. |
| 500 | FOLDER_CREATE_FAILED | Retry in a minute. |
#Rename or move a folder
Send a new name, a new parent_id, or both. parent_id: null moves it to the top level. Returns { "success": true }.
Parameters
idinteger, pathRequired- The folder id.
namestring- A new name.
parent_idinteger | null- The folder to move it into.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | FOLDER_UPDATE_EMPTY | Neither name nor parent_id. |
| 400 | FOLDER_PARENT_INVALID | parent_id is the folder itself or not a number. |
| 400 | FOLDER_PARENT_NOT_FOUND | No folder with parent_id on your account. |
| 400 | FOLDER_MOVE_INTO_SELF | parent_id is one of its own sub-folders. |
| 404 | FOLDER_NOT_FOUND | No folder with this id on your account. |
| 500 | FOLDER_UPDATE_FAILED | Retry in a minute. |
#Delete a folder
Its sub-folders move up into its parent, and its automations are taken out of any folder. No automation is deleted. Returns { "success": true }.
Errors
| HTTP | Code | When |
|---|---|---|
| 404 | FOLDER_NOT_FOUND | No folder with this id on your account. |
| 500 | FOLDER_DELETE_FAILED | The folder is still there. Retry. |
#Templates
#List templates
Ready’s built-in templates and your own, newest first. To start from one of yours, send its graph to POST /automations/from-graph. A built-in template’s graph is in the app builder’s own draft format, so open it in the app.
Returns
data.builtinobject[]- Each with
key,name,description,industries,apps,setupandgraph. data.mineobject[]- Yours, up to 100:
id,name,description,created_atandgraph.
#Save an automation as a template
Saves a copy of its graph, without the connected app accounts its steps picked. The automation doesn’t change. You can keep up to 100. Returns the template’s id in data.
Parameters
automation_idintegerRequired- The automation to save.
namestringRequired- Up to 200 characters.
descriptionstring- Up to 1,000 characters.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_ID | automation_id isn’t a valid id. |
| 400 | NAME_REQUIRED | A blank name. |
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 409 | TOO_MANY_TEMPLATES | You already have 100. Delete one first. |
| 413 | TEMPLATE_TOO_LARGE | The automation is too large to save as a template. |
| 500 | TEMPLATE_NOT_SAVED | Retry. |
#Delete a template
Deletes one of your templates. Automations made from it don’t change. Returns { "success": true }.
Errors
| HTTP | Code | When |
|---|---|---|
| 404 | TEMPLATE_NOT_FOUND | No template with this id on your account. |
| 500 | TEMPLATE_NOT_DELETED | Retry. |
#AI builder
#Draft an automation with AI
Describe what it should do, or attach screenshots of a workflow from another tool, and get back a draft. Nothing is saved: review it, then save it with Create an automation (send enabled: false to keep it off while you check it). If the goal is unclear, the first answer may be up to 2 questions instead; send your choices back in answers. Each call counts against your account’s limit on AI requests per 15 minutes.
Parameters
goalstring- What the automation should do, up to 800 characters. Send this,
images, or both. imagesobject[]- Screenshots as
{ "data": "<base64>", "media_type": "image/png" }(PNG, JPEG, GIF or WebP). The first 6 are used. With a screenshot we build without asking questions. answersobject- Your answers to its questions, keyed by each question’s
key. currentobject- An existing draft or automation to change (
name,trigger_type,trigger_config,actions), up to 2,000 characters as JSON. The answer is the whole changed automation.
Returns
dataobject- Either
{ "clarify": true, "questions": [...] }, each question withkey,question,optionsandmulti; or a draft:name,trigger_type,trigger_config,actionsandexplanation. A draft may adddropped_countanddropped_typesfor steps it couldn’t build, and, from a screenshot whose trigger we don’t have, an emptytrigger_typewith the screenshot’s words intrigger_unmatched.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | GOAL_REQUIRED | No goal and no usable image. |
| 400 | GOAL_TOO_LONG | goal is over 800 characters. |
| 400 | RULE_TOO_LARGE_FOR_AI_EDIT | current is too long to change safely. |
| 413 | IMAGES_TOO_LARGE | The screenshots are too large together. |
| 429 | rate_limited | Too many AI requests. Wait a few minutes. |
| 502 | AI_BAD_RESPONSE | The AI’s answer couldn’t be used. Try again or reword it. |
| 503 | AI_UNAVAILABLE | The AI builder isn’t available right now. |
| 503 | AI_BUDGET_PAUSED | The AI builder is paused for now. Build it step by step. |
| 500 | AI_GENERATION_FAILED | Try again. |
#Earlier versions
Each canvas save that changes an automation’s graph first keeps the graph it replaces, and each drip campaign steps save keeps the steps it replaces. You can put one back. A restore is a save too, so it can be undone the same way.
#List earlier versions
The 50 newest versions of one automation or drip campaign, newest first. For an automation, needs the manage_campaigns permission.
Parameters
kindstring, queryRequiredrulefor an automation,dripfor a drip campaign.target_idinteger, queryRequired- The automation or drip campaign id.
Returns
dataobject[]- Each with
id,created_at,step_countandreason, the save that replaced it:graph_save,steps_saveorversion_restore.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | VERSION_TARGET_INVALID | A bad kind or target_id. |
| 404 | AUTOMATION_NOT_FOUND | No automation with this id on your account. |
| 404 | DRIP_NOT_FOUND | No drip campaign with this id on your account (kind=drip). |
| 500 | VERSIONS_LOAD_FAILED | Retry. |
#Restore an earlier version
Puts that version back through the same save the builder uses, with the same checks. An automation stays on or off as it is. Needs the send_sms permission, and for an automation manage_campaigns as well. An automation answers as Save an automation’s graph does, plus the restored graph; a drip campaign answers as its steps save does. Both add kind and target_id.
Parameters
idinteger, pathRequired- The version id from the list.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | no code | A drip campaign version with no steps. |
| 404 | VERSION_NOT_FOUND | No version with this id on your account. |
| 404 | AUTOMATION_NOT_FOUND | The automation was deleted. |
| 404 | DRIP_NOT_FOUND | The drip campaign was deleted (kind=drip). |
| 500 | VERSION_RESTORE_FAILED | Nothing was changed. Retry in a minute. |
| 500 | VERSION_RESTORE_UNCONFIRMED | We couldn’t confirm it was restored. Read it again before you retry. |
See also: the guide to doing this in the app.