"When a record is added or changed, do this" is one automation with a record trigger. It works on any record type: a collection the business made, or a built-in one (customer, ticket, sf_order, reservation, lead). This page covers the trigger, what a run can read, which steps really work, and how to switch it on so it actually fires. For a lead's welcome email and waits, see New-lead follow-up. For booking follow-ups, see Build a CRM Blueprint.
Ask first
- Which records? The record type by name (the collection, or
customer,ticket, …). If it is a form's answers, the record type is the form's collection, not the form. - When? Added, changed, or deleted. For "changed", which field, and whether only a change to one value counts (e.g. status becomes
closed). - Only some records? A field that must have one value (e.g. only the
vipones, only answers from one form). - What should happen? Each step in order, and for each one who or what it reaches. Anything that emails, texts or calls people needs the words and the recipient.
Steps
1. Collection builder: the record type and its field paths
get_schema on the record type, and read one real record (find_records). Note:
- the datatype name exactly (e.g.
skilltest_rec,customer); - the field names. In a run they are under
data.(data.status,data.email). The record's id is its sk.
Hands on: the datatype and the field names. Check: the fields the person named exist on the record type. If one doesn't, ask; don't invent it.
2. Automation: the draft
Call list_automation_capabilities and list_automations (don't build a second copy). Then create_automation with status: "draft".
The trigger: { name: "database_change", config: { datatype, operation, … } }.
| config | What it does |
|---|---|
datatype | The record type. Required. |
operation | created, updated or deleted. Leave it out (or *) for all three. |
whereField / whereValue | Only records where this field has this value. A path on the whole record: data.status, owner.id. Compared as text. Works on created and updated. |
changedField | Updates only: run only when this field's value changed in this save (data.status). |
changedTo | With changedField: only when it changed to this value (closed). |
data_created, data_updated and data_deleted (the names Studio's builder saves) work the same way. lead_created, ticket_created, reservation_created, task_created and invoice_created are created on their record type.
fieldFiltersanddataFiltersare saved but ignored when the engine matches a record, and so is Studio's Watch Specific Fields box. UsechangedFieldandwhereField.ticket_updated,ticket_resolved,reservation_updated,lead_qualifiedand the other "…_updated / …_won / …_paid" names are accepted but nothing ever fires them. Usedatabase_changewithoperation: "updated"andchangedField.form_submittednever fires for a form with a collection behind it. Trigger on the collection withwhereField: "owner.id",whereValue: "<form name>"(see Registration that creates the customer).- Records of type
automation,automation_execution,history,setting,session,logand any type starting with_never trigger anything.
What a run can read. Put these in any text as {{…}}:
| Variable | Created / updated | Deleted |
|---|---|---|
{{data.<field>}} | the record's fields, as saved now (after the change) | empty |
{{dataId}}, {{record.sk}} | the record's id | empty |
{{record.<field>}} | the whole record: record.datatype, record.owner.id, record.createdate | empty |
{{operation}} | created / updated | deleted |
{{triggerEvent}} | data_created / data_updated | data_deleted |
{{dataType}} | the datatype | the datatype |
{{createdBy}} | the email of whoever created the record (also on an update) | system |
{{timestamp}} | when the run started | when the run started |
- There are no previous values. On an update,
{{data.status}}is the new value.{{previousValues.…}},{{changedFields}}and{{oldData.…}}don't exist.changedField/changedToin the trigger are the only way to act on what changed. - A deleted record is gone before the run starts. The run knows the datatype and nothing else: no id, no fields, and a
whereFieldcan't match. Don't promise "when X is deleted, tell its owner". - A
{{variable}}that doesn't resolve stays as the text{{variable}}. It is saved or sent like that. Only use fields every record has. - Every value comes out as text.
"{{data.score}}"saves"20", not20, and a list saves asa,b. - A step can't read an earlier step's result. The sk of a record made by
create_data, a task's id, a delay's time: none of them reach later steps.
The steps. Each is { type: "action" | "condition", name, id, config }. Give every step an id, because conditions jump to them.
| Step | config | What it really does |
|---|---|---|
create_data | datatype, data: { field: "{{data.x}}" } | Saves a new record. It fires that record type's created automations. On customer it is refused (Customer should be created using user api) unless data.accountType: "contact". |
update_data | collection, filter, update | Finds records with filter (a query, e.g. { "sk": "{{dataId}}" } or { "data.email": "{{data.email}}" }) and writes update to the first one (options: { multi: true } for all). Write update with dot keys: { "data.note": "…" }; to add to or take from a list, { "$addToSet": { "data.tags": "vip" } } / $pull ($inc, $push, $unset work too, $set fails). Works on customer too. It does not fire updated automations. |
delete_data | collection, filter | Deletes every match. An empty filter is refused. |
add_tag / remove_tag | tags: ["vip"] | Adds or removes tags on the record the run is about (data.tags). Saves the record in full, which fires its updated automations. Fails on a customer ("Customer should be updated using user api"): tag customers with update_data and $addToSet. |
create_note | content | Adds a note to the record the run is about. Saves the record in full, which fires its updated automations. Fails on a customer the same way. |
create_task | title, description, priority (low…urgent), assignedTo (an email), dueDate | A task on the record, in the assignee's list, with a notice in their bell. |
add_to_customer_group | group (a name or {{data.company}}), create | Only when the run is about a customer. Adds the group's name to the customer's groups and the customer to the group's contacts. Makes the group (static) the first time unless create: false; a smart group is refused; an empty {{…}} skips the step. |
create_ticket | title, description, priority, reportedByEmail, assignTo | A real ticket, numbered, and the reporter is emailed. An unresolved {{…}} fails the step. |
send_email / send_sms | to: "{{data.email}}", subject, body (or templateIds), from | Sends to real people. See Stop. |
send_webhook | url, method (default POST), headers, body | Calls another system; {{…}} in the body are filled. |
delay | duration, unit (minutes, hours, days, weeks), skipWeekends | Pauses the run; the next step runs after the wait. |
end_flow | none | Ends the run. Use it as an elseStepId. |
These names are accepted, report success, and do nothing: update_ticket, update_lead, qualify_lead, disqualify_lead, convert_lead, create_reservation, cancel_reservation, calculate_score, add_activity, get_data. Don't build on them. To change a ticket or lead field, use update_data on ticket / lead.
Conditions.
| Condition | config |
|---|---|
if_then | condition: { field: "data.score", operator, value }, thenStepId, elseStepId. Operators: equals, not_equals, contains, not_contains, greater_than, less_than, greater_than_or_equal, less_than_or_equal, exists, not_exists, in, not_in, in_array, not_in_array, starts_with, ends_with. field is a path in the run (data.x, record.owner.id), without {{ }}. |
record_exists | datatype, match: { "data.email": "{{data.email}}" } (case is ignored), optional createdAfter, thenStepId (found), elseStepId (not found). If a match value is empty for this record, the step fails and the run stops. |
How branches run:
- A step named by a condition's
thenStepIdorelseStepIdis entered only by that jump. When the run reaches the end of one branch and the next step in the list is another branch's first step, the run ends there. - Always give a condition both
thenStepIdandelseStepId. With noelseStepId, a false condition carries on into the next step, which is usually the "then" step. Point "else" at anend_flowstep when nothing should happen.
An automation can trigger itself, up to 3 times. An automation on created of a type that also create_datas that type starts itself again; so does one on updated that runs add_tag, remove_tag or create_note on the same record. The engine's loop guard lets one automation run at most 3 times in one chain of runs, each started by the run before; the 4th is skipped and logged as run-skipped in Automation › Runs & Logs › All Logs (tested: 3 runs, 3 records made, then skipped). That is still 3 records, emails or notes where 1 was meant. Narrow an updated automation with changedField, or write with update_data, which fires nothing.
A worked example, a ticket closing:
{
"name": "Ticket closed → note and tag",
"status": "draft",
"trigger": { "name": "database_change",
"config": { "datatype": "ticket", "operation": "updated", "changedField": "data.status", "changedTo": "done" } },
"steps": [
{ "id": "is_urgent", "type": "condition", "name": "if_then",
"config": { "condition": { "field": "data.priority", "operator": "equals", "value": "urgent" },
"thenStepId": "review_task", "elseStepId": "done" } },
{ "id": "review_task", "type": "action", "name": "create_task",
"config": { "title": "Review urgent ticket {{data.name}}", "description": "{{data.title}}", "priority": "high", "assignedTo": "<manager's email>" } },
{ "id": "done", "type": "end", "name": "end_flow", "config": {} }
]
}Hands on: the automation's sk.
Check: call_appengine GET /automation/<sk>/check. It answers { "ready": true, "problems": [] }, or names each step that can't run and why. Open the automation in the builder (/automation?openId=<sk>).
Don't add or re-save steps in Studio's builder. Its Update Record step saves { datatype, recordId, fields }, which the engine refuses: Start answers WORKFLOW_NOT_READY ("Required field 'collection' is missing", "'filter'…", "'update'…"). Build update_data with create_automation.
3. Automation: try it once on one record
Make or pick one test record (names starting test-, emails @test.local), then run the draft once against it:
call_appengine POST /automation/<sk>/execute with { "variables": { "datatype": "<datatype>", "recordId": "<record sk>", "dataId": "<record sk>" } }.
A draft may run this way. The engine loads that record, so {{data.…}} and {{record.…}} resolve as they will for real. Without variables, a database_change trigger runs with nothing (every {{data.…}} stays literal). A data_created / data_updated trigger takes the newest record of the type instead.
Stop: a test run really does every step. If the automation emails, texts, calls or creates customers, test it on a copy without those steps, or with the person's own address as the recipient, and say so first.
Hands on: the run's executionId (in the answer).
Check: GET /automation/executions/<executionId>. results[] has one entry per step with success and what it did (the record create_data made, matchedCount / modifiedCount for update_data, branchPath for a condition). Look for any {{ left in what was written: that variable did not resolve.
4. Automation: switch it on with Start
Stop: from now on it runs for every matching record. Show the person:
- the record type, the operation and the narrowing (
whereField,changedField/changedTo); - each step, and who it reaches (emails, texts, tasks, customers made);
- whether it can trigger itself (the loop warning above).
Wait for a clear yes. Then call_appengine POST /automation/start/<sk>.
Start is what makes it listen. Saving status: "active" (set_automation_status, create_automation with status: "active", or a save in the builder) only changes the status: the trigger isn't connected, and the automation doesn't run until someone presses Start or the engine restarts. The monitor shows this as "Switched on but not listening" with a Reconnect trigger button, which is Start.
To switch it off: POST /automation/stop/<sk>. That also stops runs waiting in a delay.
Hands on: the automation's sk, running.
Check: GET /automation/<sk>/monitor answers health: { "triggerRegistered": true, "triggerKeys": ["data_updated:ticket"] }.
5. Automation monitor: the first real run
Change one test record the way the trigger expects (create it, or set its status to the changedTo value). Within a few seconds:
GET /automation/<sk>/runslists the run:status(completed,failed,pausedwhile in a delay),stepsDone,failedStepId,error, andsubject(the record's name and email);GET /automation/executions/<executionId>shows each step's result and the variables it ran with.
The same is on screen at /automation?monitor=<sk>: runs per day, errors grouped by step with Fix this step, each step's outcomes, and the runs log. A run can be stopped (POST /automation/executions/<id>/cancel) or run again with the same record (…/retry).
Check: the run is completed, every step did what it should, and nothing ran that the condition should have skipped. Delete the test records, and anything the run made from them.
When it's done
Tell the person:
- the record type and exactly when it runs (operation,
whereField,changedField/changedTo); - each step, and who or what it reaches;
- that it was tried on a test record, and what the run did;
- whether it is running (started) or still a draft.
Open the monitor (/automation?monitor=<sk>).
What it can't do yet
- "From X to Y". There is no previous value, so "only when status goes from open to closed" can't be checked.
changedTochecks only the new value. - Act on a deleted record's fields. A deleted run doesn't see the record.
- Use one step's result in the next. A record made by
create_datacan't be updated, linked or emailed by id later in the same run. - Changes made without a full save don't trigger. Automations'
update_data, the ticket status / assign / priority / escalate buttons, workflow stage moves, bulk imports and partial updates (update-partial) fire no updated automations. A ticket closed with its status button doesn't run achangedTo: "done"automation. - A delay sees the record as it was. After a
delay,{{data.…}}is still the record from when the run started, not its current values. wait_until_conditionchecks the run's saved copy of the record, not the live record, so "wait until the status changes" doesn't work.- Stop a self-trigger at the first run. The loop guard only stops a chain after 3 runs. See the warning in step 2.
Don'ts
- Don't create it
active, even thoughcreate_automationdefaults to active. Passstatus: "draft", and switch it on withPOST /automation/start/<sk>, notset_automation_status. - Don't use
fieldFilters,dataFilters,previousValues,changedFieldsor{{lead.…}}/{{contact.…}}. They do nothing. - Don't leave a condition without
elseStepId. - Don't trust "success" from the stub actions listed above.
- Don't run a test of an automation that sends, without saying who will receive it.