Step types
Every workflow step type, with its parameters, the output later steps can read, how it fails, and an example you can copy.
Last updated
A workflow is a chain of steps. Each step does one job, saves its output under its key, and passes control to its on success or on failure link. This page lists every step type: what it needs, what it returns, and when it fails. For the model behind jobs and links, see How workflows work.
| Step | Dashboard label | action value |
Output |
|---|---|---|---|
| Read data | Read Data | ReadData |
data: the records |
| Create data | Create Data | CreateData |
data: the created records |
| Update data | Update Data | UpdateData |
data: the payloads sent |
| Upsert data | Upsert Data | UpsertData |
data: one record per item |
| Delete data | Delete Data | DeleteData |
None |
| Condition | Condition | Condition |
condition_result and details |
| Run a script | Run Script | RunScript |
data: the list your script returns |
| Call an API | Call An API | CallAnApi |
data: the parsed response |
| Send an email | Send an Email | SendAnEmail |
to, template_type, variables, attachment_count |
| Send a push notification | Send a Push Notification | SendPushNotification |
Delivery counts |
| File handler | File Handler | FileHandler |
file_id, url and more |
| Integration | Integrations tab | Integration |
The service's result |
Parameter names are snake_case, and the same parameters object is what the dashboard saves, what --params takes in the CLI, and what the workflow API stores.
Add a step
Every step has a key of letters, numbers and underscores, which later steps use to read its output. One step is the start step.
In the Anythink dashboard
- Open the workflow and select Add Step.
- Choose the Action card, or open the Integrations tab for Claude, OpenAI, Grok, Slack, GitHub, Google Calendar, LinkedIn or X.
- Enter a Name and a Key, tick Enable Step, and fill in the fields the step shows.
- Save the step. Connect Start to the first step.
- Drag from a step's green handle to the step that should follow on success, and from its red handle to the step that should follow on failure.
- Select Save Workflow, then Enable Workflow.
With the CLI
anythink workflows step-add 42 summarise \
--name "Summarise orders" \
--action RunScript \
--start --enabled \
--params '{"script":"return { count: $anythink.steps.find_orders.data.length };"}'
anythink workflows step-link 42 <summarise_step_id> --on-success <next_step_id> --on-failure <alert_step_id>
Replace 42 with the workflow's id. step-add prints the new step's id. To change a step later, use anythink workflows step-update <workflow_id> <step_id> --params '<json>'. In --params JSON, a payload is a string that holds JSON, so its quotes are escaped.
Two helpers build the longer steps from named flags: workflows file-handler-add for File handler and workflows integration-add for Integration.
The CLI calls the workflow steps API, which your own code can call too. It needs anythink_workflows:create to add a step, anythink_workflows:update to change one and anythink_workflows:delete to remove one.
| Endpoint | Does |
|---|---|
POST /workflows/{id}/steps |
Adds a step to a workflow. Returns 201 and the step. |
PUT /workflows/{id}/steps/{stepId} |
Replaces a step's settings. Returns 200 and the step. |
DELETE /workflows/{id}/steps/{stepId} |
Removes a step. Returns 204. |
The raw request:
POST /org/{project_id}/workflows/42/steps
Content-Type: application/json
{
"key": "summarise",
"name": "Summarise orders",
"enabled": true,
"action": "RunScript",
"parameters": { "script": "return { count: $anythink.steps.find_orders.data.length };" },
"is_start_step": false,
"on_success_step_id": 108
}
key, name and action are required, and parameters is the object for that step type. description is optional. key must be unique in the workflow and can't be changed afterwards. on_success_step_id and on_failure_step_id link to other steps in the same workflow. The response carries the new step's id.
PUT takes the same body without key. It replaces the step's settings rather than merging them, so send every value you want to keep, including both links and the whole parameters object. A link you leave out is cleared.
A locked workflow can't be changed, and an unknown workflow or step id returns 400 with a message saying so.
With an AI assistant (MCP)
In workflow 42, add a Run Script step called summarise that counts the records from find_orders, make it follow find_orders on success, and link its failure path to the alert step.
The assistant runs workflows step-add and workflows step-link through the cli tool.
How steps return data
A step's output is saved under its key, so a step with the key find_orders is read with {{ $anythink.steps.find_orders… }}. Where the output lives depends on the step:
| Output shape | Steps | Read it with |
|---|---|---|
A data list |
Read data, Create data, Update data, Upsert data, Run a script | $anythink.steps.<key>.data[0].field |
A single data value |
Call an API: the parsed response, an object or a list | $anythink.steps.<key>.data.field |
| Fields at the top level | Condition, Send an email, Send a push notification, File handler, Integration | $anythink.steps.<key>.field |
| Nothing | Delete data | Not available |
Templates in {{ }} are resolved immediately before a step runs. See Template syntax for every expression.
When a step fails
A step fails when its parameters don't validate or its action can't finish. The job records the error message, and the workflow follows the step's on failure link. With no on failure link, the job ends as Failed. When the failure path leads to steps that finish, the job ends as Success and the failed step stays marked as failed in the job history.
Some steps save details when they fail, so a failure path can use them. Call an API keeps status_code and response, and Condition keeps its evaluation details. Read them like any output, for example {{ $anythink.steps.call_partner.status_code }}. The failure notes under each step say which.
Anythink doesn't retry a failed step. See Test workflows and read job history for reading a failed job.
Filters in data steps
Read data, Update data and Delete data select records with filter_conditions, a list of conditions that must all match:
[
{ "field": "status", "operator": "eq", "value": "pending" },
{ "field": "created_at", "operator": "lt", "value": "{{ $anythink.now }}" }
]
operator |
Matches when the field |
|---|---|
eq, neq |
Equals, or doesn't equal, value |
contains, sw, ew |
Contains, starts with, or ends with value |
gt, gte, lt, lte |
Is greater or less than value |
in |
Is one of a list in value, such as ["paid", "shipped"] or paid,shipped |
value is a string and can contain templates. A condition with a missing field, or an operator that isn't in this table, fails the step before anything is read or deleted. Values are URL-encoded for you.
Note: A Delete data step removes every record the filter matches. Check the same filter in a Read data step first.
Row-level security options
Steps run as the workflow worker, so records they create have no owner and aren't readable by users on entities with row-level security. Create data, Update data and Upsert data can grant access as part of the write:
| Parameter | What it does |
|---|---|
auto_set_rls |
Grants the user ids in the payload field named by rls_user_field (default user) read and write access |
auto_set_rls_groups |
Grants the group ids in the payload field named by rls_group_field (default group) read and write access |
auto_set_rls_user_groups |
Resolves the groups the rls_user_field user belongs to. Related options are rls_user_group_mode, rls_user_group_id, rls_user_group_grant, rls_user_groups_readonly and rls_user_group_field. |
rls_mode |
Update data only. add (the default), remove or set the grants |
If a field named for a grant is missing or can't be read, the step logs a warning and writes the record without that grant. The defaults and a worked example are in Groups and row-level security.
Read data
Reads records from an entity. Reads ignore row-level security, so filter to the records the workflow should act on.
| Parameter | Required | Notes |
|---|---|---|
entity |
Yes | The entity name. This step uses entity, where the other data steps use entity_name. |
limit |
Yes | The most records to read, at least 1 |
filter_conditions |
No | See Filters in data steps. Without it, every record is read. |
fields |
No | Comma-separated field names. All fields by default. |
sort_by |
No | A field name |
sort_direction |
No | asc or desc |
Output: data, the list of records, each with its id. A read that matches nothing succeeds with an empty list.
Fails when: entity or limit is missing, a filter is invalid, or the read is rejected.
{
"entity": "orders",
"filter_conditions": [{ "field": "status", "operator": "eq", "value": "pending" }],
"sort_by": "created_at",
"sort_direction": "asc",
"limit": 100
}
Later steps read {{ $anythink.steps.find_orders.data[0].total }}.
Create data
Creates one record, or one per item in a list.
| Parameter | Required | Notes |
|---|---|---|
entity_name |
Yes | |
payload |
Yes | A string holding a JSON object, or a list of objects. Templated. |
loop_over |
No | A template that resolves to a list, such as {{ $anythink.steps.find_orders.data }}. The payload is filled in once per item, and the item's fields are available without a prefix: {{ id }}, {{ email }}. |
prevent_duplicates |
No | false by default. Skips an item when a record already matches all of its non-empty fields. |
| Row-level security | No | See Row-level security options |
Output: data, the created records as saved, each with its id. If every item was a duplicate, data is an empty list.
Fails when: the payload or loop_over can't be parsed, or a create is rejected. Records created before the failure stay.
{
"entity_name": "order_reviews",
"loop_over": "{{ $anythink.steps.large_orders.data }}",
"payload": "{ \"order_id\": \"{{ id }}\", \"note\": \"Review orders over £100\" }",
"prevent_duplicates": true
}
Update data
Updates records by id or by filter.
| Parameter | Required | Notes |
|---|---|---|
entity_name |
Yes | |
payload |
Yes | A string holding a JSON object. Templated. With filters, it's filled in once per matched record, and that record's fields are available without a prefix. |
ids |
One of ids or filter_conditions |
Comma-separated ids, templated. Takes priority over filters. |
filter_conditions |
One of ids or filter_conditions |
See Filters in data steps |
| Row-level security | No | Includes rls_mode. See Row-level security options. |
Output: data, the payloads that were sent, one per updated record. It isn't the saved records.
Fails when: ids resolves to no numbers, neither ids nor filter_conditions is set, or an update is rejected. The step refuses to run without one of them, so it can't update every record by accident. Records updated before the failure stay.
{
"entity_name": "orders",
"ids": "{{ $anythink.trigger.id }}",
"payload": "{ \"status\": \"reviewed\", \"reviewed_at\": \"{{ $anythink.now }}\" }"
}
Upsert data
For each item, finds a matching record by key fields, then updates it, leaves it alone, or creates a new one. Use it for imports and enrichment, where the same data may arrive more than once.
| Parameter | Required | Notes |
|---|---|---|
entity_name |
Yes | |
payload |
Yes | A string holding a JSON object or a list. Templated. |
match_on |
Yes | The fields that identify a record, such as ["external_id"] |
on_match |
No | Ignore (the default) returns the existing record. Update writes the payload to it. |
update_fields |
No | With Update, write only these fields. All fields by default. |
loop_over |
No | As in Create data |
| Row-level security | No | Applied to created and updated records |
Output: data, one record per item in the order of the input, which suits mapping data[i].id onto other records. With Update, the returned record includes the new values.
Fails when: an item is missing a match_on field, or a write is rejected. Items are processed one at a time, so earlier items stay written. When more than one record matches, the oldest is used.
{
"entity_name": "customers",
"loop_over": "{{ $anythink.steps.fetch_customers.data }}",
"payload": "{ \"external_id\": \"{{ id }}\", \"email\": \"{{ email }}\", \"name\": \"{{ name }}\" }",
"match_on": ["external_id"],
"on_match": "Update",
"update_fields": ["email", "name"]
}
Delete data
Deletes every record that matches the filter.
| Parameter | Required | Notes |
|---|---|---|
entity_name |
Yes | |
filter_conditions |
Yes | At least one condition |
Output: none, so later steps can't read it. The step log records how many were deleted.
Fails when: no filter is given, a filter is invalid, or a delete is rejected. The step collects every matching id first, then deletes them in batches.
{
"entity_name": "sessions",
"filter_conditions": [{ "field": "expires_at", "operator": "lt", "value": "{{ $anythink.now }}" }]
}
Condition
Branches the workflow. When the conditions hold, the workflow follows on success. Otherwise the step fails and the workflow follows on failure.
| Parameter | Required | Notes |
|---|---|---|
filter_conditions |
Yes | At least one. field is a template path such as $anythink.steps.find_orders.data[0].total, and value can contain templates. |
logical_operator |
No | AND or OR. AND by default. |
The operators are eq, neq, contains, sw, ew, gt, gte, lt, lte, is_null and is_not_null. Text compares without regard to case, and gt, gte, lt and lte compare as numbers when both sides are numbers.
Output: condition_result, logical_operator and evaluation_details. The details list each comparison with its expected_value, actual_value and result, which helps when a branch goes the wrong way.
Fails when: the conditions don't hold, which is the normal way to take the false branch. The evaluation details are still saved.
{
"logical_operator": "AND",
"filter_conditions": [
{ "field": "$anythink.trigger.data.total", "operator": "gte", "value": "100" }
]
}
Note: A false result with no on failure link ends the job as Failed. When "nothing to do" is a normal outcome, link the false branch to a harmless step.
Run a script
Runs JavaScript for logic the other steps can't express: reshaping data, calculations, building prompts, parsing responses.
| Parameter | Required | Notes |
|---|---|---|
script |
Yes | The body of a function. Use return to output data. |
The script reads a $anythink object:
| Property | Contents |
|---|---|
$anythink.trigger |
id, entity_name and data |
$anythink.steps |
Every earlier step's output, by key |
$anythink.workflow |
id, name, description, created_at and updated_at |
$anythink.job |
id, and step with the running step's key, name, description, action and parameters |
console.log, info, warn, error and debug write to the step's log. isArray, isObject and deepClone are available as helpers.
| The script returns | Output |
|---|---|
| An object | data is a list holding that object |
| A list | data is that list |
| Nothing | No output |
| A number, text or boolean | The step fails. Return an object instead. |
| (throws an error) | The step fails |
Fails when: the script throws, returns a number, text or boolean, or runs past its limits.
var orders = $anythink.steps.find_orders.data;
var total = orders.reduce(function (sum, o) { return sum + o.total; }, 0);
return { count: orders.length, total: total, over_budget: total > 5000 };
Later steps read {{ $anythink.steps.summarise.data[0].total }}.
Scripts run in a sandboxed JavaScript interpreter with a 30-second timeout, 64 MB of memory and 100,000 statements. They have no network or file access, templates aren't applied inside scripts, and secrets aren't available. Use Call an API or Integration for anything external.
Call an API
Sends one HTTP request and keeps the response. Webhooks walks through a full example with a secret.
| Parameter | Required | Notes |
|---|---|---|
url |
Yes | An absolute http or https address on the public internet. Templated. Template values aren't URL-encoded, so encode them yourself. |
method |
Yes | GET, POST, PUT or DELETE, in capitals |
headers |
No | An object of header names to values. Values are templated. |
body |
No | Templated. Sent as application/json. |
results_path |
No | A dot path into the JSON response to keep, such as data.items. Without it the whole response is kept. |
Output: data, the parsed JSON response or the part at results_path. An empty response, or one that isn't JSON, succeeds with no data or an empty object, so check it in a Condition when it matters.
Fails when: the status isn't 2xx, the request takes more than 30 seconds, the address is private, local or not http or https, or results_path isn't found. A failure saves status_code and response. Anythink doesn't sign or retry the request.
{
"url": "https://api.example.com/v1/customers?updated_since={{ $anythink.steps.last_sync.data[0].synced_at }}",
"method": "GET",
"headers": { "Authorization": "Bearer {{ $anythink.secrets.EXAMPLE_API_KEY }}" },
"results_path": "customers"
}
Keep credentials in a header with a secret, as above, and never in the URL, because request URLs are written to the step log. See Store API keys and tokens as secrets.
Send an email
Sends one of your project's email templates to one recipient. Domains, senders, templates and attachments are covered in Send email from a workflow.
| Parameter | Required | Notes |
|---|---|---|
to |
Yes | Templated. Must resolve to an email address. |
from |
No | A sender address. It's used when it's on one of your verified sending domains, and otherwise the default sender is used. |
template_type |
Yes | The template's type |
payload |
No | A string holding a JSON object of template variables. Templated. {} by default. |
attachments |
No | Up to 10 entries of file_id, disposition (attachment or inline) and content_id |
Output: to, template_type, variables and attachment_count. The step finishes when the email is queued for sending.
Fails when: to is empty, still holds an unresolved expression or isn't an email address, from is set but isn't a valid address, payload isn't valid JSON, or there are more than 10 attachments.
{
"to": "{{ $anythink.trigger.data.email }}",
"template_type": "welcome",
"payload": "{ \"first_name\": \"{{ $anythink.trigger.data.first_name }}\" }"
}
Send a push notification
Sends a push notification through your configured providers and waits up to 30 seconds for the delivery result. Providers, devices, templates and action buttons are covered in Push notifications from a workflow.
| Parameter | Required | Notes |
|---|---|---|
title, body |
Yes, unless template_type is set |
Templated |
template_type |
No | A saved push template that supplies any content you leave empty |
image_url, click_action |
No | Templated |
category |
No | An action-button group registered for your project |
buttons |
No | A list of { "id", "label", "click_action" } |
data |
No | An object of extra string values delivered with the notification |
audience.kind |
No | user (the default), users, entity or all |
audience.user_id |
For user |
Defaults to the user_id in the trigger data |
audience.user_ids |
For users |
A list, or a variable that resolves to one |
audience.owner_entity_name, audience.owner_entity_id |
For entity |
The users with access to that record. The id defaults to the trigger id. |
audience.provider |
No | fcm or web_push. Without it, every configured provider is used. |
Send once per row. When the title, body or another content field refers to $anythink.steps.<key>.data[*], the step sends one notification per item in that step's data, with the item's fields available by name. Up to 10 send at a time.
Output: send_log_id, delivery_status, delivered, failed, unregistered, title, body and audience_kind. A partial delivery also adds delivery_error. A per-row send returns iterations, the totals and send_logs.
Fails when: the audience is missing or empty, a template expression doesn't resolve, or delivery fails. A per-row send fails only if every send fails.
{
"title": "{{ $anythink.steps.due_checkins.data[*].first_name }}, time for your check-in",
"body": "It takes two minutes.",
"audience": { "kind": "user", "user_id": "{{ $anythink.steps.due_checkins.data[*].user_id }}" }
}
File handler
Moves files in and out of records.
| Parameter | Required | Notes |
|---|---|---|
operation |
Yes | FetchAndLink, LinkExisting or Detach |
entity_name, record_id, field_name |
Yes | The record and the file field to change. record_id is templated and field_name is snake_case. |
source_url |
For FetchAndLink |
A public http or https address to download. Templated. |
existing_file_id |
For LinkExisting |
A file already in your project. Templated. |
file_name, folder_id |
No | Templated. The name defaults to one taken from the URL. |
is_public |
No | false by default |
link_mode |
No | set (the default) replaces the field's files. add appends. |
overwrite_if_present |
No | false by default. With set, replace a file that's already there. |
skip_if_empty_source_url |
No | false by default. Succeed without doing anything when source_url resolves to nothing. |
Downloads are limited to 50 MB and 30 seconds, and private or local network addresses are refused.
Output: entity_name, record_id, field_name and skipped, which is true when an existing file was kept. FetchAndLink also returns file_id, url, mime_type, extension and size_bytes. LinkExisting also returns file_id.
Fails when: a parameter doesn't validate, the download fails or is too large, or the record can't be updated.
{
"operation": "FetchAndLink",
"entity_name": "products",
"record_id": "{{ $anythink.trigger.id }}",
"field_name": "image",
"source_url": "{{ $anythink.trigger.data.image_url }}",
"skip_if_empty_source_url": true
}
The CLI builds the same step from flags:
anythink workflows file-handler-add 42 fetch_photo --operation fetch_and_link \
--source-url '{{ $anythink.trigger.data.image_url }}' \
--entity products --record-id '{{ $anythink.trigger.id }}' --field image --enabled
Integration
Calls a connected service with your project's or a user's credentials. Connect services first. See How integrations work and Integration providers and operations for the inputs of each operation.
| Parameter | Required | Notes |
|---|---|---|
provider |
Yes | See the table below |
operation |
Yes | |
credential_source |
No | system (the default) uses the project's connection. current_user uses the connection of the user who started the job. connection uses the connection named in connection_id. |
connection_id |
For connection |
The connection's id |
inputs |
No | An object of operation inputs. Text values are templated. |
| Provider | Operations |
|---|---|
claude, openai, grok |
generate-text, analyse-image, analyse-document, summarise, translate. OpenAI also has generate-image. |
slack |
send-message, send-direct-message |
github |
list-commits, list-pull-requests, list-releases, list-issues, get-file-contents, get-repo-tree, list-workflow-runs (read-only) |
google-calendar |
create-event, list-events, get-event, update-event, delete-event |
linkedin |
create-post |
twitter |
create-tweet |
Output: the service's result, at the top level. AI text operations return text, model, input_tokens and output_tokens, read with {{ $anythink.steps.<key>.text }}.
Fails when: no credentials are connected for the provider (the error names the service to connect), the operation is unknown, or the service rejects the call.
{
"provider": "claude",
"operation": "generate-text",
"credential_source": "system",
"inputs": {
"max_tokens": "2000",
"system_prompt": "Summarise customer feedback in two sentences.",
"prompt": "{{ $anythink.trigger.data.feedback }}"
}
}
The CLI builds the same step from flags:
anythink workflows integration-add 42 summarise_feedback \
--provider claude --operation generate-text --credential-source system \
--input 'system_prompt=Summarise customer feedback in two sentences.' \
--input 'prompt={{ $anythink.trigger.data.feedback }}' --enabled