powered by

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

  1. Open the workflow and select Add Step.
  2. Choose the Action card, or open the Integrations tab for Claude, OpenAI, Grok, Slack, GitHub, Google Calendar, LinkedIn or X.
  3. Enter a Name and a Key, tick Enable Step, and fill in the fields the step shows.
  4. Save the step. Connect Start to the first step.
  5. 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.
  6. Select Save Workflow, then Enable Workflow.

With the CLI

bash
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:

http
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:

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

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

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

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

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

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

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

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 };

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.

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

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.

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

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

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.

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
}

The CLI builds the same step from flags:

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

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

bash
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

Next steps