Automations
What your own system receives from an automation, the API request as the Workflow user and the structure of a flow
Last updated on
Automations let Tillor carry out actions itself, such as creating tasks or calling your system. The user guide is at Automations. This page describes what an integrator sees: the message from the Send webhook step, the API request to your own URL or to the Tillor API, and how a flow is built if you manage it through the API.
Send webhook
The Send webhook step delivers a custom message to one of the organisation's webhooks. It is the same delivery as for ordinary webhook events, with the same signature.
The message
The step does a POST with Content-Type: application/json and these headers:
| Header | Content |
|---|---|
X-Webhook-Event-ID | A unique ID for this delivery. The value stays the same on new attempts, so you can deduplicate on it |
X-Webhook-Signature | sha256=<hex>, the HMAC-SHA256 of the body with the webhook's signing secret |
The body has the same envelope as the webhook payload:
{
"event": "automation.invoice_unpaid",
"data": {
"invoice": "2026-0042",
"amountOutstanding": 125.5
},
"timestamp": 1735689600000,
"traceId": "0b0f7c0e-5d3c-4c1e-9b8a-7f1e2a3b4c5d",
"triggeredBy": null
}| Field | Description |
|---|---|
event | The Event name you filled in in the step. Any text is allowed, the name does not have to be in the list of event keys |
data | The Payload from the step, after the variables have been filled in, as JSON |
timestamp | Unix time in milliseconds of the delivery |
traceId | A random UUID per delivery. Unlike events from the system, this is not a trace ID of a Tillor action |
triggeredBy | Always null |
There is no spanId. The webhook does not have to be subscribed to an event key: a message from an automation is always delivered to the chosen endpoint, as long as it exists and is enabled.
How to verify the signature is described under Verifying the signature.
Attempts and outcome
Tillor makes up to 5 attempts within the step, waiting 1, 2, 4 and 8 seconds between them. An attempt counts as successful on a 2xx status. A target that is blocked, for example an internal address, also counts as a failed attempt.
- If one of the attempts succeeds, the flow continues through the Succeeded exit
- If all five fail, the flow continues through Failed. The message is then retried in the background according to the schedule under Delivery behaviour and retries. If such a later attempt succeeds, it no longer shows up in the run, because the run has already moved on
If the webhook no longer exists or is disabled, the step does not go through Failed. The run then stops with the status Failed.
API request to your own URL
With API request and Send to set to Custom URL, an automation sends an HTTP request to your server.
| Part | Behaviour |
|---|---|
| Method | GET, POST, PUT, PATCH or DELETE |
| URL | Must start with https://. Variables are filled in encoded |
| Body | JSON. Not sent for GET and DELETE. With a body, Tillor sets Content-Type: application/json |
| Headers | Free to choose. The values may contain variables. Host, Content-Length, Transfer-Encoding and Connection are ignored |
| Target | Private and internal addresses are refused, even if an ordinary domain name points to them |
| Redirects | Are not followed. A 3xx response counts as failed |
| Timeout | 15 seconds |
No signature
Unlike Send webhook, Tillor does not sign this request. If you want to check that a request comes from your automation, put a secret in a header and check it on your side. Headers are stored encrypted, so a key or token in them is safe.
A response with a 2xx status continues through Succeeded, any other status through Failed. If the server cannot be reached, or exceeds the timeout, the run stops with the status Failed.
API request to the Tillor API
With Send to set to Tillor API, the automation calls the API of its own organisation. You only give a path, for example /tasks, and Tillor places it after /api/orgs/{orgId}. Your API reference is at OpenAPI.
- Identity: the request is executed as the organisation's Workflow user. That user has all permissions within that organisation and nowhere else. Changes are therefore recorded under the name Workflow
- No key needed: Tillor makes the call itself, without an API key and without going over the internet
- Path: must start with a single
/.//,..and\are not allowed. A query string is allowed, for example/tasks?terrainId=…&limit=1. Variables in the path are filled in encoded - Blocked: the paths
/automationsand/automation-runsare refused, also with a query string, capital letters, a trailing/or percent-encoding. An automation cannot manage automations - Body: JSON, for
POST,PUTandPATCH. ForGETandDELETEit is not sent - Picker: in the editor there is Pick an API endpoint with the operations from the OpenAPI specification. Internal operations are not listed. If you pick one, Tillor fills in the method, path and an example body
- Failure: a 2xx response continues through Succeeded. An error status, including a 404 for a path that does not exist, goes through Failed
The response in later steps
After the request these variables are available:
| Variable | Content |
|---|---|
{response.status} | The HTTP status code |
{response.body} | The first 4000 characters of the response as text |
{response.data} | The response as JSON. null if it is not JSON, or if it is longer than 100,000 characters. You address lists with an index, for example {response.data.items.0.id} |
A second API request in the same flow replaces response. The variables work as described under Variables and templates.
The structure of a flow
You can also create and change automations through the API. The operations are in OpenAPI under the tag automations. Reading requires the permission automations:read, creating and changing automations:write (or all permissions). The Workflow user itself can never call them. It may be easier to build a flow in the editor and read it as an example.
A new automation is always disabled. Enabling it through enabled does not work as long as the flow has open problems, and an enabled flow cannot be saved with problems.
The definition consists of steps (nodes), lines (edges) and optionally notes (notes). Maximum 100 steps, 200 lines and 50 notes.
{
"nodes": [
{
"id": "trigger",
"type": "trigger",
"label": "",
"position": { "x": 0, "y": 0 },
"config": { "source": "event", "eventKey": "occupation:ended" }
},
{
"id": "task",
"type": "taskCreate",
"label": "",
"position": { "x": 0, "y": 190 },
"config": { "title": "Controleer {occupation.terrain.name}", "priority": "HIGH" }
}
],
"edges": [
{ "id": "trigger:out:task", "source": "trigger", "target": "task", "sourceHandle": "out" }
],
"notes": [
{
"id": "note_1",
"text": "Maakt na elk vertrek een controletaak.",
"position": { "x": 340, "y": 0 },
"width": 240,
"height": 140
}
]
}A note (notes) is an explanation on the canvas. It is never executed and has no lines. text is at most 2000 characters, width and height are in pixels.
A line has a sourceHandle: the exit of the step it comes from.
Step (type) | Exits (sourceHandle) |
|---|---|
trigger, delay, waitUntilOpen, waitUntilClosed, taskCreate, taskSetStatus, taskSetPriority, taskUpdate, invoiceReminderSend, invoiceSend, invoicePrint, occupationExtendInvoice, staffNotify, commentCreate, inboxReply, inboxTag, customerUpdate, accessMethodCreate, accessMethodUpdate, accessMethodSendQr, accessPointControl, meterSwitch, nfcTagUpdate, deviceCommand, deviceDisplay, deviceSlideshow, controllerRelay, controllerCommand | out |
customerMessageSend, reservationSetStatus, invoiceBook, paymentLinkCreate, mandateRequest, mandateCollect, consumptionInvoice | out, skipped |
condition | yes, no |
switch | The id of each branch, plus default |
openingHours | open, closed |
throttle | pass, blocked |
taskFind, documentFind, recordLookup | found, notFound |
forEach | each, done |
waitUntil | met, timeout |
waitForEvent | received, timeout |
approval | approved, rejected, timeout |
webhookSend, httpRequest | success, failed |
Every action except webhookSend and httpRequest gets an extra exit error as soon as continueOnError in the config is set to true. Without that setting a failed action stops the run. With that setting the flow continues through error, with {error.message} and {error.step} as variables for the steps after it. The exit skipped is used when a step had nothing to do, and out is then the ordinary exit.
For forEach, the exit each starts a separate run per item for the steps after it, and the exit done is the ordinary continuation of the flow itself.
An exit may lead to several steps. The run itself continues with the first line and starts a separate run for every other line with the same data, which runs in parallel (like the exit each of forEach). Lines that form a loop are refused in the editor. A flow has exactly one trigger.
Settings per step
The fields under config are the fields from the editor. Text fields accept variables between curly braces.
| Step | Fields in config |
|---|---|
trigger | source (event, webhook, invoiceOverdue, dateReached, daily or cron), webhookVersion (for webhook: text, increase it to replace the address), eventKey, date (for dateReached: a path record.field to a DateTime column of a model that an event sends in full, such as reservation.checkIn or contact.birthday, or to a declared date key in a JSON column, such as document.userMetadata.insuranceExpiryDate; old names such as reservationCheckIn keep working), dateOffset ({ "amount", "unit" }), dateDirection (before or after), dateTime (exact: the hour in the date, or fixed: the calendar day of the date in the organisation's time zone, at dateHour and dateMinute, text such as "9" and "0"; the offset counts on the clock, so across summer and winter time), repeatYearly (boolean: only day and month count, every year), hour and minute (text, for example "8" and "0", minute 0 to 59 inclusive), cron (five fields; the weekday may be 1#2 or 1L, a sixth field W2:2026-10-05 repeats every two weeks), conditions, oncePerRecord (boolean) |
condition | conditions |
switch | field (a variable path without braces), cases: a list of { "id", "key", "value" } with key as the name of the branch and value as the value the field must equal. Maximum 8 branches |
delay | mode (duration or untilDate), duration and offset as { "amount", "unit" } where unit is seconds, minutes, hours, days or weeks, dateField (variable path), direction (before or after) |
taskCreate | title, description, assigneeIds, labelIds, assignActor, priority (LOW, MEDIUM, HIGH or URGENT), viewId, laneId, subtasks (one per line), dueIn ({ "amount", "unit" }), linkRecords |
taskSetStatus | taskId, status (PENDING, IN_PROGRESS, WAITING or COMPLETED) |
taskFind | title (exact title), linkedToRecords, onlyOpen, onlyTopLevel (booleans) |
documentFind | customerId (template), documentType (INSURANCE_POLICY, INSURANCE_PAYMENT_PROOF, IDENTITY_CARD, SEPA_MANDATE, OTHER or empty), onlyActive, onlyValid (booleans). Result under foundDocument |
documentUpdate | documentId (template), documentType (empty keeps the type), status (ACTIVE, ARCHIVED or empty) |
taskSetPriority | taskId, mode (raise, lower or set), priority (for set), maxPriority (for raise) |
taskUpdate | taskId, title, assigneeIds, assignActor, assigneeMode (add or replace), labelIds, dueIn ({ "amount", "unit" } where unit is minutes, hours, days or weeks) |
invoiceReminderSend, invoiceSend, invoiceBook | invoiceId |
invoicePrint | invoiceId, printer (a4, pos or tablet) |
paymentLinkCreate | invoiceId, method (any, BANCONTACT, IDEAL, CREDITCARD or PAYCONIQ) |
mandateRequest | customerId, mode (request or reminder) |
mandateCollect | invoiceId, delayDays (text, default "0", at most 7) |
consumptionInvoice | customerId, sendEmail, sendSms |
occupationExtendInvoice | occupationId, includeConsumption |
customerMessageSend | customerId, subject, body, sendEmail, sendSms, sendWhatsApp |
staffNotify | userIds, notifyActor, title, body, sendPush, sendEmail |
commentCreate | content, target (event or task), taskId (for task) |
inboxReply | threadId, body, keepAssistant |
inboxTag | threadId, tagIds, archive |
customerUpdate | customerId, status (keep, DEFAULT, PREFERRED, VIP, WARNING or BLACKLISTED), notes |
reservationSetStatus | reservationId, action (checkIn, checkOut or cancel) |
accessMethodCreate | customerId, type (QR_CODE, LPR, PIN_CODE or NFC), value, periodFrom, periodTo (variables with a date, empty is no limit) |
accessMethodUpdate | scope (one or customer), accessMethodId (for one), customerId (for customer), status (keep, ACTIVE, SOFT_BLOCKED, BLOCKED or INACTIVE), periodTo |
accessMethodSendQr | accessMethodId, sendEmail, sendSms, sendWhatsApp, print (none, label or thermal) |
accessPointControl | accessPointId, action (open or close) |
meterSwitch | target (terrain or meter), terrainId (for terrain), meterId (for meter), state (on or off) |
nfcTagUpdate | customerId, action (block, unblock, topUp or lowBalance), amount (for topUp, in euros per tag) |
deviceCommand | deviceId, command (wake, sleep, reload, identify, testSound or playSound), url (for playSound: the path of an uploaded sound, as the editor fills it in, or an https link to an mp3, wav or m4a file) |
deviceDisplay | kind (pdf or image), url (https link). Appears on every tablet that is set up as a display, such as an invoice PDF |
controllerRelay | target (controller or event: the controller of the event controller:input:changed that started the flow), controllerId (for controller), relay (text "1" to "6"), action (pulse, on or off), durationSeconds (for pulse, text, 0.001 to 3600 with decimals such as "0.5"; the relay drops out again by itself) |
controllerCommand | target and controllerId as for controllerRelay, command (reboot, rebootExtension, otaUpdate, forceOtaUpdate, extensionOtaUpdate or refreshSettings) |
deviceSlideshow | scope (displays or device), mode (screensaver or background), source (feed or urls). For feed: itemsPath, imageField, optional filter and sort fields |
openingHours, waitUntilOpen, waitUntilClosed | No fields |
throttle | window ({ "amount", "unit" } where unit is minutes, hours, days or weeks), key (empty is per record that started the flow) |
recordLookup | model (Customer, Reservation, Occupation, Invoice, Payment, Terrain, Task, Document, AccessMethod, NfcTag, Meter, InboxThread or InboxMessage), recordId. The result is under found plus the model, such as foundCustomer |
forEach | source (customerAccessMethods, customerNfcTags, customerOpenInvoices, customerUpcomingReservations, customerOccupations, customerDocuments (only ACTIVE) or terrainMeters), parentId (ID of the customer or the terrain). At most 100 items, the item is under item |
waitUntil | conditions, interval and timeout ({ "amount", "unit" } where unit is minutes, hours, days or weeks; the interval is at least 5 minutes) |
waitForEvent | eventKey, sameRecord (boolean), timeout ({ "amount", "unit" }) |
approval | title, description, assigneeIds, timeout ({ "amount", "unit" }). The task is under approvalTask.id |
Every action except webhookSend and httpRequest | continueOnError (boolean) |
webhookSend | webhookId, event, payload (JSON as text) |
httpRequest | target (tillor or custom), method, path (for tillor), url (for custom), headers (list of { "id", "key", "value" }), body (JSON as text) |
conditions has the form { "match": "all" or "any", "rules": [...] }. Each rule is { "id", "field", "operator", "value", "values" }. operator is is, isNot, contains, greaterThan, lessThan, isOneOf, isEmpty or isNotEmpty. You only use values with isOneOf. An empty list of rules lets everything through.
Start an automation with a webhook
An automation with the trigger webhook starts on a POST to its own address:
curl -X POST "https://tillor.eu/api/orgs/org_abc123/automation-hooks/aut_xxx/<token>?bron=website" \
-H "Content-Type: application/json" \
-d '{"email":"jan@voorbeeld.nl","aankomst":"2026-07-14"}'- The full address is in the editor at the trigger, or via
GET /automations/{id}/webhook-url(withautomations:read). The token is the secret; a header for authentication is not needed - The JSON body becomes the variable
body, the query stringquery, and the timereceivedAt - The answer is
200with{ "accepted": true }as soon as the run has started. A wrong, replaced or disabled address gives404, a body above 100 KB gives400 - Many calls shortly after each other are not refused: every valid call starts a run. Use Ignore repeats at the trigger if the same call can arrive twice, see Ignore repeats
An automation cannot call its own webhook through an API request to the Tillor API.
Choose a decision through the API
The task of an Ask for a decision step has one action per option you set in the step. Fetch the task to see the actions with their id, and choose one with POST /tasks/{id}/actions/{actionId}/choose. If the option asks for a note, send it along as { "note": "..." }. The choice is final: it completes the task and continues the automation along the exit of that option. Assigned colleagues (also through a group) and administrators may choose. See the OpenAPI for the details.
Read more
- Webhooks for signature, retries and the list of events
- HTTP API for authentication and the organisation context
- Variables and templates for the user guide