powered by

Step types

Every workflow step, its parameters, what it returns to later steps, and when it fails.

Each step does one job. Configure steps on the workflow canvas, or send the same parameters object to the workflow API. Parameter names are snake_case.

How steps return data

A step's output is saved under its key, so a step with 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, Call an API $anythink.steps.<key>.data[0].field
Fields at the top level Condition, Send an email, Send a push notification, File handler, Integration $anythink.steps.<key>.field
Nothing Delete data —

When a step fails, its error details are saved at the top level of its output, and the workflow follows the step's on failure link if it has one.

Filters in data steps

Read data, Update data and Delete data select records with filter_conditions, a list of conditions that must all match:

json
[
  { "field": "status", "operator": "eq", "value": "pending" },
  { "field": "created_at", "operator": "lt", "value": "{{ $anythink.now }}" }
]
operator Meaning
eq, neq Equals, doesn't equal
contains, sw, ew Contains, starts with, ends with
gt, gte, lt, lte Greater or less than
is_null, is_not_null Empty or not empty (no value)

value can contain templates.

Warning: Operators other than these are ignored rather than rejected. For a Delete data step, a filter whose operators are all misspelled matches every record. Test delete steps against a copy of your data first.

Note: Values are added to the query as they are. Avoid values containing & in filters until this is handled.

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 Default What it does
auto_set_rls false Grant the users in rls_user_field read and write access
rls_user_field user Payload field holding a user id, a comma-separated list of ids, or a list
auto_set_rls_groups false Grant the groups in rls_group_field read and write access
rls_group_field group Payload field holding group ids
auto_set_rls_user_groups false Grant access to the groups the rls_user_field user belongs to
rls_user_group_mode all all of that user's groups, or selected to use rls_user_group_id
rls_user_group_id — Group id for selected mode. Templated.
rls_user_groups_readonly true Group members can read but not edit
rls_user_group_grant false Also write a group grant, so every member of the group can read the record
rls_user_group_field — Field to stamp with the user's group id, when the user belongs to exactly one group
rls_mode add Update data only: add, remove or set the grants

A payload can also carry grants directly:

json
{
  "title": "Weekly check-in",
  "user_id": 21,
  "_rls": { "mode": "add", "users": [{ "user_id": 21, "readonly": false }] }
}

If a field named for grants is missing or can't be read, the step logs a warning and writes the record without that grant.

Limit: Update data ignores the row-level security options when it updates by ids. Use filters, or an _rls payload, to change grants.


Read data

Reads records from an entity.

Parameter Required Default Notes
entity yes — Entity name. Note: entity, not entity_name.
filter_conditions no none See Filters in data steps
limit no 5000 Maximum records. Fetched 1,000 at a time.
fields no all Comma-separated field names
sort_by no — Field name
sort_direction no — asc or desc

Limit: Omitting limit currently fails validation with "Limit must be greater than or equal to 0" instead of falling back to 5000. Set limit explicitly until this is fixed.

Limit: anythink workflows step-add and step-update both silently rewrite a Read data step's entity parameter to entity_name, which then fails with "Entity is required". Neither CLI command can set a working Read data step yet — set parameters with a direct PUT /org/{orgId}/workflows/{workflowId}/steps/{stepId} call instead, until this is fixed.

Returns data: the list of records.

json
{
  "entity": "orders",
  "filter_conditions": [{ "field": "status", "operator": "eq", "value": "pending" }],
  "sort_by": "created_at",
  "sort_direction": "asc",
  "limit": 100
}

Reads ignore row-level security. Filter to the records the workflow should act on.

Create data

Creates one record, or one record per item in a list.

Parameter Required Default Notes
entity_name yes —
payload yes — A JSON object, or a list of objects. Templated.
loop_over no — A template resolving 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 Skip an item when a record already matches all of its non-empty fields
Row-level security no See Row-level security options

Returns data: the created records, or an empty list if every item was a duplicate. Fails when loop_over doesn't resolve to a list or a create fails.

json
{
  "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 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 filters Comma-separated ids, templated. Takes priority over filters.
filter_conditions one of ids or filters
Row-level security no Includes rls_mode. Filters only.

Returns data: the payloads that were sent (not the saved records). Fails when ids resolves to no numbers, or when neither ids nor filters are set.

json
{
  "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, or creates a new one. Use it for imports and enrichment, where the same data may arrive more than once.

Parameter Required Default Notes
entity_name yes —
payload yes — JSON object or list. Templated.
match_on yes — List of fields that identify a record, such as ["external_id"]
on_match no Ignore Ignore returns the existing record; Update writes the payload to it
update_fields no all fields On Update, only write these fields
loop_over no — As in Create data
Row-level security no Applied to created and updated records

Returns data: one record per item, in the order of the input. Fails when an item is missing a match_on field.

When more than one record matches, the oldest (lowest id) is used and a warning is logged. Items are processed one at a time; if one fails, earlier items stay written.

json
{
  "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 matching the filter.

Parameter Required Notes
entity_name yes
filter_conditions yes At least one condition

Returns nothing, so later steps can't reference it. All matching ids are collected first and then deleted.

json
{
  "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 it follows on failure.

Parameter Required Default Notes
filter_conditions yes — field is a template path, such as $anythink.steps.find_orders.data[0].total; value can be templated
logical_operator yes AND AND or OR

Text compares without regard to case. gt, gte, lt and lte compare as numbers when both sides are numbers.

Returns condition_result, logical_operator and evaluation_details, which lists each comparison with its actual value — useful when a branch goes the wrong way.

json
{
  "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 normal, link the false branch to a harmless step.

Limit: is_null and is_not_null don't work reliably in conditions yet, because missing values are compared as text. Compare with eq and an empty value, or test for emptiness in Run a script.

Run a script

Runs JavaScript for logic that 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 can read a $anythink object:

Property Contents
$anythink.trigger id, entity_name and data
$anythink.steps Every earlier step's output, by key
$anythink.workflow id, tenant_id, name, description, created_at, updated_at
$anythink.job id and the current step

console.log, info, warn, error and debug write to the step's log. isArray, isObject and deepClone are available as helpers.

Script returns Output
An object data is a list containing 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 and follows on failure
javascript
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 };

Scripts run on the Jint 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 an HTTP request.

Parameter Required Notes
url yes Templated. Template values aren't URL-encoded, so encode them yourself.
method yes GET, POST, PUT or DELETE, in capitals
headers no Object of header names to values. Values are templated.
body no Templated. Sent as application/json.
results_path no Dot path into the response to keep, such as data.items

Returns data: the parsed JSON response, or the part at results_path. Fails on a non-2xx status, with status_code and response in the output.

json
{
  "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"
}

Note: An empty response stores no output. A response that isn't JSON, or a results_path that isn't found, succeeds with data set to {} — check the result in a Condition when it matters.

Note: Put credentials in headers, not in the URL. Request URLs are written to the step log.

Send an email

Sends one of your project's email templates. See Email templates.

Parameter Required Default Notes
to yes — Templated; must resolve to an address
from no project default Used only when it's on one of your verified sending domains
template_type yes — The template's key
payload no {} JSON object of template variables. Templated.
attachments no — Up to 10 { "file_id", "disposition", "content_id" } entries

Returns to, template_type, variables and attachment_count. Success means the email was queued for sending, not that it was delivered.

json
{
  "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.

Parameter Required Notes
template_type no A push template that supplies any content you leave empty
title, body yes, unless a template is set Templated
image_url, click_action no Templated
category no An action-button category registered for your project
buttons no [{ "id", "label", "click_action" }]
data no Object of extra values delivered with the notification
audience.kind no user (default), users, entity or all
audience.user_id for user Defaults to the trigger data's user_id
audience.user_ids for users One notification to every user in the list
audience.owner_entity_name, audience.owner_entity_id for entity Users with access to that record. The id defaults to the trigger id.
audience.provider no fcm, web_push or apns

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 once.

json
{
  "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 }}" }
}

Returns send_log_id, delivery_status, delivered, failed and unregistered; per-row sends return iterations and totals. The step fails if delivery fails — for per-row sends, only if every send fails.

File handler

Moves files in and out of records.

Parameter Required Default Notes
operation yes — FetchAndLink, LinkExisting or Detach
entity_name, record_id, field_name yes — The record and file field to change. Templated.
source_url for FetchAndLink — Public http(s) URL to download
file_name, folder_id no from the URL Templated
is_public no false
link_mode no set set replaces the field's files; add appends
overwrite_if_present no false With set, replace a file that's already there
skip_if_empty_source_url no false Succeed without doing anything when source_url is empty
existing_file_id for LinkExisting — Templated

Downloads are limited to 50 MB and 30 seconds, and private or local network addresses are refused.

Returns file_id, url, mime_type, size_bytes and skipped, which is true when an existing file was kept.

json
{
  "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
}

Integration

Calls a connected service with your project's or a user's credentials. Connect services in Settings › Integrations.

Parameter Required Default Notes
provider yes — See the table below
operation yes —
credential_source no system system (project connection), current_user, connection or entity_field
connection_id for connection — Templated
credential_field_path for entity_field — Template resolving to stored credentials
inputs no {} Operation inputs. Text values are templated.
Provider Operations
claude, openai, grok generate-text, analyse-image, analyse-document, summarise, translate; OpenAI adds 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

Returns 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 }}.

json
{
  "provider": "claude",
  "operation": "generate-text",
  "credential_source": "system",
  "inputs": {
    "model": "claude-opus-4-8",
    "max_tokens": "2000",
    "system_prompt": "Summarise customer feedback in two sentences.",
    "prompt": "{{ $anythink.trigger.data.feedback }}"
  }
}

Limit: Claude models that start their reply with a thinking block, including Claude Opus 5, currently return empty text. Use claude-opus-4-8 until support lands. With no credentials connected, the step fails and tells you which service to connect.

Next steps

Workflow recipes Monitoring and testing