How workflows work
Workflows run your backend logic in Anythink. A trigger starts a job, the job follows a chain of steps, and each step can use what came before it.
Last updated
Workflows are Anythink's automation engine. Use them for the logic that would otherwise need a backend service: reacting to new records, running nightly jobs, exposing an endpoint that does several things at once, calling other APIs, and sending email or push notifications. Nothing to host, and no server code.
This page explains the model and what happens at runtime. To build one, start with Build your first workflow.
The moving parts
| Part | What it is |
|---|---|
| Workflow | A named automation with one or more triggers and a set of steps. Names are unique within a project. |
| Trigger | What starts the workflow: a data event, a schedule, a button or an API call. A workflow can have several, and any of them starts the same steps. |
| Step | One action: read data, create a record, call an API, run a script, send a notification. Each step has a key, which is how later steps refer to its output. |
| Job | One run of a workflow. It records the payload that started it, every step's log and output, and the final status. |
Steps are connected, not just ordered. Each step has an on success link and an optional on failure link to another step. One step is the start step. A job begins there and follows links until a step has nowhere left to go. In the editor, the green handle on a step is on success and the red handle is on failure.
What happens when a workflow runs
- A trigger fires. A record is created or updated, a schedule comes due, someone runs the workflow by hand, or your code calls its API route.
- Anythink creates a job with the status Pending and a payload of
{ id, entity_name, data }, then queues it for the workflow worker. For an event trigger with a filter, the filter is checked first. If it doesn't match, no job is created. - The worker picks the job up. It checks your plan's workflow quota and sets the job to Running.
- It runs the start step. Templates in the step's parameters, such as
{{ $anythink.trigger.data.email }}, are resolved just before it runs. - The step's output is saved. Later steps can read it as
$anythink.steps.<key>.data. - The worker follows a link. A successful step follows on success. A failed step follows on failure.
- The job finishes. See Success and failure paths.
Jobs run on a separate worker, not in your request. A record you create returns straight away, and the job follows moments later.
Triggers
| Trigger | Starts a job when | Typical use |
|---|---|---|
| Event | A record is created or updated, a user registers or is invited, a payment or subscription changes, or a push notification button is tapped | Welcome emails, follow-up tasks, syncing data |
| Timed | A cron schedule comes due | Nightly imports, weekly reports, reminders |
| Manual | Someone runs it from a record in the dashboard, or your code calls the trigger endpoint | Admin actions, one-off fixes |
| API call | A POST reaches /org/{project_id}/workflows/api/{route} |
Custom endpoints for your app |
A trigger that watches an entity must name an entity that exists. The workflow has an Enable Workflow setting, and each trigger has its own Enabled setting, so you can switch one trigger off without touching the others.
Event triggers
Pick an Event Type. The dashboard shows the names below with spaces, and the CLI and API use them as written.
| Event | Fires when | What the job receives |
|---|---|---|
EntityCreated, EntityUpdated |
A record in the chosen entity is created or updated | $anythink.trigger.id is the record's id and entity_name is the entity. The data is the record. |
UserRegistered, UserInvited |
A user registers with your project or is invited to it | $anythink.trigger.id is the user's id. The data has first_name, last_name, email and referral_code. |
PaymentCreated, PaymentSucceeded, PaymentFailed |
AnythinkPay records a payment | The payment. $anythink.trigger.id is 0. |
SubscriptionCreated, SubscriptionActivated, SubscriptionExpired |
A subscription is created, activated or expires | The subscription. $anythink.trigger.id is 0. |
PaymentMethodCaptured, PaymentMethodCaptureFailed, PaymentMethodRemoved |
A saved payment method is captured, fails to capture or is removed | The payment method. $anythink.trigger.id is 0. |
PushActionTaken |
Someone taps a button on a push notification | The button's group, the action, the user and any extra data the app sent. See Push notifications from a workflow. |
On a record event, the trigger data doesn't include the id, so read the record's id from $anythink.trigger.id. Payment and subscription events are raised for Stripe activity. See Webhooks for the details.
An event trigger can carry a filter so the workflow only runs when it matters, for example only when an order's status changes to paid. A filter compares a field with a value using =, ≠, >, ≥, <, ≤, in, contains, is null or is not null. On an update it can also test whether a field changed or changed to a value. Conditions combine with All of these or Any of these. Details are in Triggers and scheduling.
Timed triggers
A timed trigger uses a standard five-field cron expression, such as 0 6 * * * for 06:00 every day. Schedules are in UTC, and Anythink checks for due schedules once a minute, so the finest resolution is one minute. A scheduled job has no record behind it: $anythink.trigger.id and $anythink.trigger.data are empty. Each due schedule starts one job, and missed runs aren't caught up.
Manual triggers
A manual trigger lists the entities it applies to. Those entities' records get a button in the Anythink dashboard that runs the workflow on that record. Your code can do the same by calling the workflow's trigger endpoint, which needs the anythink_workflows:trigger permission and takes the entity name, record id and data you send.
API call triggers
An API call trigger gives the workflow its own route. Callers POST JSON to it with an API key, the body becomes $anythink.trigger.data, and the response is 204 No Content as soon as the job is queued. The request doesn't wait for the result. Webhooks has the URL, the rules for routes, the permission the caller needs and an end-to-end example. To create a key for the caller, see API keys.
Steps
Each step has a name, a key and an action. The key can contain letters, numbers and underscores, and later steps use it in templates. The actions are:
| Step | What it does |
|---|---|
| Read Data | Reads records from an entity, with filters, sorting and a limit |
| Create Data | Creates one record, or one per item in a list |
| Update Data | Updates records by id or by filter |
| Upsert Data | Matches records on key fields, then updates, ignores or creates |
| Delete Data | Deletes every record matching a filter |
| Condition | Branches the workflow on one or more comparisons |
| Run Script | Runs JavaScript against the trigger data and earlier step outputs |
| Call An API | Sends an HTTP request and keeps the JSON response. See Webhooks. |
| Send an Email | Sends one of your project's email templates. See Send email from a workflow. |
| Send a Push Notification | Sends to a user, a list of users, a record's owners or everyone, once or per row. See Push notifications from a workflow. |
| File Handler | Downloads a file into Anythink and attaches it to a record, links an existing file, or detaches one |
| Integration | Calls a connected service: Claude, OpenAI, Grok, Slack, GitHub, Google Calendar, LinkedIn or X. See How integrations work. |
Every parameter and output is listed in Step types. For ready-made patterns, see Workflow recipes.
Passing data between steps
Parameters can contain templates in double braces. Anythink resolves them just before the step runs.
| Template | Value |
|---|---|
{{ $anythink.trigger.id }} |
The id of the record or user that fired the trigger |
{{ $anythink.trigger.data.email }} |
A field from the trigger data. Use dots for nested objects and square brackets for list items, such as items[0].sku. |
{{ $anythink.steps.find_customer.data[0].name }} |
A field from an earlier step's output. Steps that return records or script results put a list under data, so use data[0] for the first item. |
{{ $anythink.secrets.PARTNER_API_KEY }} |
A project secret, looked up when the step runs. See Secrets. |
{{ $anythink.now }} |
The current time, in UTC, in ISO 8601 format |
When a template is the whole value of a JSON field, the value keeps its type, so a number stays a number and an object stays an object. Inside longer text it's written into the text. If a path doesn't resolve, the expression is left as written, so add a fallback with | default:
{{ $anythink.trigger.data.nickname | default: "there" }}
The full rules are in Template syntax.
Success and failure paths
A step succeeds when its action completes and fails when it doesn't. A failure might be a rejected parameter, a script error, or a non-2xx response from a Call An API step. A Condition step that evaluates to false counts as a failure too, which is how you branch: link on success to the "true" path and on failure to the "false" path.
| What happens | Job status |
|---|---|
| The last step that ran succeeded | Success |
| A step failed and its on failure link led to steps that finished | Success. The failed step is still marked failed in the history. |
| A step failed and has no on failure link | Failed, with that step's error message |
| The start step is missing, a link points to a step that no longer exists, or the job runs more than 250 steps | Failed, with a message saying which |
| Your plan's workflow quota is used up | Failed, with a quota message. The run isn't counted. |
Plan the "nothing to do" outcome. If a Condition is false and has no on failure link, the job ends Failed. Link the false branch to a step, even a harmless one, when that outcome is normal.
Retries
Anythink doesn't retry anything. A failed step either follows its on failure link or ends the job, and a failed job stays failed. To run it again, trigger the workflow again with the same payload. The job history keeps the payload you need. To react to a failure instead, send yourself an email or a message from the on failure path.
Jobs and history
Every run is recorded in the workflow's Job History, so you can see exactly what each step received and returned.
| Job status | Meaning |
|---|---|
| Pending | Queued, waiting for the worker |
| Running | The worker is executing steps |
| Success | Finished as described above |
| Failed | A step failed, or the job couldn't complete |
A job keeps the payload that started it, an error message if it failed, and for every step that ran its success flag, log, output and error. Open it in the Anythink dashboard from View Job History in the workflow editor, or list a workflow's jobs with anythink workflows jobs <workflow_id>. To try a workflow without waiting for its trigger, use Test Workflow in the editor, or run anythink workflows trigger <workflow_id> --payload '<json>'. The payload appears as $anythink.trigger.data. Test workflows and read job history covers reading jobs and tracking down failures.
Who a workflow acts as
Workflow steps run as the Anythink workflow worker, a trusted service, not as the person who started the job. Design for two consequences:
- Reads ignore row-level security. A Read Data step sees every record in the entity. Filter explicitly to the user or group you're acting for, for example
user_idequals{{ $anythink.trigger.data.user_id }}. - Records created by a workflow have no owner. On entities with row-level security, grant access in the step itself with its row-level security options. See Step types and Groups and row-level security.
Anyone who can edit a workflow can therefore read and send any data in the project. Give the workflow permissions (anythink_workflows:create, :update, :delete and :trigger) only to people you'd trust with that access. See Roles and permissions.
Limits
| Limit | Value |
|---|---|
| Steps run per job | 250. A job that loops past this fails with a maximum-step-limit message. |
| Retries | None. See Retries. |
| Schedules | Five-field cron, UTC only, one-minute resolution. |
| Run Script | 30 seconds, 64 MB and 100,000 statements per run. No network access. |
| Call An API | 30 seconds per request. http and https addresses on the public internet only. |
| File downloads | 50 MB and 30 seconds per file. |
| Runs per month | Set by your plan's workflow quota. |
| Names | A workflow name is unique in the project and up to 255 characters. A step key is up to 255 characters. |
| API call responses | 204 No Content. The job runs in the background, and the response doesn't include its result. |
| Job order | Not guaranteed. Jobs run concurrently. |
| Sub-workflows | Not supported. Put shared logic in each workflow, or have one workflow write a record that another workflow's event trigger watches. |
Behaviour worth knowing
- Workflows can trigger workflows. Create Data and Update Data steps write through the normal API, so they fire event triggers, including on the entity the workflow itself watches. Use a trigger filter to stop a workflow from starting itself again.
- A step that runs twice replaces its output. If your links loop back to a step, later steps see the most recent run's output.
- Several workflows can watch the same event. Each one starts its own job.
Next steps
- Build your first workflow: build and test one in about ten minutes
- Triggers and scheduling: events, filters, schedules, manual buttons and API routes
- Step types: every step, parameter and output
- Template syntax: every
{{ }}expression a step accepts - Test workflows and read job history: read jobs and fix failures
- Webhooks: start a workflow from an HTTP call, or call another system
- Secrets: keep API keys out of your steps