Complete workflows that combine triggers, data steps, scripts, notifications and integrations — with the reasoning behind each part so you can adapt them.
Each recipe is a full workflow you can rebuild and change. They show the patterns that come up most: importing data without duplicates, messaging many users individually, adding AI to a record, and keeping team data private.
Goal: keep a customers entity in step with an external CRM, updating existing customers and adding new ones, without creating duplicates when the job runs twice.
Trigger: Timed, 0 2 * * * (02:00 UTC).
| Key | Step | Why |
|---|---|---|
fetch_customers |
Call an API | Pull customers changed in the last day from the CRM |
upsert_customers |
Upsert data | Match on the CRM's id, update what changed, create what's new |
fetch_customers:
{
"url": "https://api.examplecrm.com/v2/customers?updated_within=24h",
"method": "GET",
"headers": { "Authorization": "Bearer {{ $anythink.secrets.crm_api_key }}" },
"results_path": "customers"
}
upsert_customers:
{
"entity_name": "customers",
"loop_over": "{{ $anythink.steps.fetch_customers.data }}",
"payload": "{ \"crm_id\": \"{{ id }}\", \"email\": \"{{ email }}\", \"name\": \"{{ full_name }}\", \"plan\": \"{{ plan }}\" }",
"match_on": ["crm_id"],
"on_match": "Update",
"update_fields": ["email", "name", "plan"]
}
How it works:
loop_over runs the payload once per customer, with each customer's fields available by name.match_on: ["crm_id"] makes the import safe to re-run: a customer that already exists is updated, never duplicated.update_fields stops the import overwriting fields your team edits in Anythink.Adapt it: give crm_id a unique constraint so a manual edit can't create a second match, and add a File handler step if the CRM provides avatars — FetchAndLink with skip_if_empty_source_url: true.
Goal: every morning, remind users who haven't checked in this week, using their own name.
Trigger: Timed, 0 8 * * *.
| Key | Step | Why |
|---|---|---|
due_checkins |
Read data | Find users whose last check-in is more than a week old |
remind |
Send a push notification | One notification per user, with their name |
due_checkins reads a checkin_status entity filtered on last_checkin_at lt your cut-off date, returning user_id and first_name.
remind:
{
"title": "{{ $anythink.steps.due_checkins.data[*].first_name }}, how was your week?",
"body": "Your check-in takes two minutes.",
"click_action": "/check-in",
"audience": { "kind": "user", "user_id": "{{ $anythink.steps.due_checkins.data[*].user_id }}" }
}
How it works:
data[*] in the title and audience makes the push step send once per row, with each row's fields filled in. Up to ten send at a time.iterations: 0, so a quiet morning isn't a failed job.Goal: when a ticket is created, add a two-sentence summary and a suggested priority.
Trigger: Event, EntityCreated on tickets.
| Key | Step | Why |
|---|---|---|
summarise |
Integration | Ask Claude for a summary and a priority, as JSON |
parse |
Run a script | Turn Claude's reply into fields |
save |
Update data | Write them back to the ticket |
summarise:
{
"provider": "claude",
"operation": "generate-text",
"credential_source": "system",
"inputs": {
"model": "claude-opus-4-8",
"max_tokens": "1000",
"system_prompt": "You triage support tickets. Reply with only JSON: {\"summary\": \"two sentences\", \"priority\": \"low\" | \"normal\" | \"urgent\"}",
"prompt": "Subject: {{ $anythink.trigger.data.subject }}\n\n{{ $anythink.trigger.data.body }}"
}
}
parse:
var text = $anythink.steps.summarise.text || "";
var start = text.indexOf("{"), end = text.lastIndexOf("}");
if (start < 0) throw new Error("No JSON in reply: " + text.slice(0, 200));
var reply = JSON.parse(text.slice(start, end + 1));
return { ai_summary: reply.summary, ai_priority: reply.priority };
save:
{
"entity_name": "tickets",
"ids": "{{ $anythink.trigger.id }}",
"payload": "{{ $anythink.steps.parse.data[0] }}"
}
How it works:
$anythink.steps.summarise.text.save is one template, so the script's object is written as-is.save updates by $anythink.trigger.id because trigger.data on a create doesn't include the new id.Note: Use
claude-opus-4-8for now. Models that begin their reply with a thinking block, including Claude Opus 5, currently return empty text through the Integration step.
Goal: when a coach logs a session note for an athlete, the athlete and everyone in their team can read it, but other teams can't.
Trigger: Event, EntityCreated on session_logs.
One Create data step writes the shared note with row-level security set from the athlete:
{
"entity_name": "session_notes",
"payload": "{ \"user_id\": \"{{ $anythink.trigger.data.athlete_id }}\", \"note\": \"{{ $anythink.trigger.data.summary }}\" }",
"auto_set_rls": true,
"rls_user_field": "user_id",
"auto_set_rls_user_groups": true,
"rls_user_group_mode": "all",
"rls_user_groups_readonly": true,
"rls_user_group_grant": true,
"rls_user_group_field": "team_group_id"
}
How it works:
auto_set_rls with rls_user_field gives the athlete read and write access.auto_set_rls_user_groups looks up the athlete's groups; rls_user_group_grant shares the note with every member, read-only.rls_user_group_field stamps the team's group id on the note, so you can filter and report by team.Adapt it: leave rls_user_group_grant off when only staff should see team records; staff can still read them through group-scoped reads without every team member seeing each other's notes. See Roles and permissions.