Integrating · Published 2026-09-23 · Updated 2026-10-12 · AgentiSend
Automations
A trigger and ordered steps, or a graph with delays, waits and conditions; a version saved on enable, and every run recorded step by step.
An automation comes in one of two shapes. The first is a trigger and an ordered list of steps: when the trigger matches, the steps run for one contact, in the order you listed them. The second is a graph, in the shape Resend's API uses: steps with a key, a type and a config, joined by connections, with delays, waits for an event and conditions that branch (see The graph). Either way the run is recorded step by step. Create one with POST /automations — "Create an automation: a trigger plus ordered steps (email, webhook, A/B split), or Resend’s graph of steps and connections (delay, wait for event, condition, send email, contact update and delete, add to segment)." — then turn it on with POST /automations/{id}/enable. There is no visual editor; in the console, create is a form and the detail page writes the trigger and each step as a sentence. A draft body:
{
"name": "Shipping notice",
"trigger": { "type": "custom_event", "event_name": "order.shipped" },
"steps": [
{
"kind": "email",
"from_address": "orders@yourdomain.com",
"subject": "Your order has shipped",
"content": { "mode": "markdown", "source": "Hi {{first_name}}, it is on its way." }
}
]
}Who it writes to
The only recipient an email step has is the contact the trigger names. There is no to field, so an automation cannot address someone who is not already a contact, and a contact is someone who gave you their address for this. A step for a contact whose status is not active is skipped, and the run says why: skipped: contact is unsubscribed. Automation mail is marketing mail. It carries the one-click unsubscribe headers and runs every send check POST /emails runs, so a suppression, a spent plan inclusion or an unverified domain stops it. Nothing here makes cold outreach easier.
Triggers
trigger.type is one of:
custom_event, with theevent_nameyour code posts. Without one the draft is refused with "custom_event triggers need an event_name."contact.createdandcontact.updated, fired byPOST /contacts.segment.joinedandsegment.left, with an optionalsegment_id. They fire when a contact write or a rule change moves someone in or out, and on a sweep that re-checks membership.scheduled, withinterval_minutes. A scheduled run has no contact, so its email steps are skipped and its webhook steps still run.
Custom events
Your code reports an event with POST /events: a name, the contact_id it concerns, and a flat data object. Every enabled automation that matches runs before the reply comes back; the request is answered as accepted, with fired, the number of automations that ran. An event with no contact_id still fires, but its email steps are skipped because there is nobody to write to.
curl -sS -X POST https://api.agentisend.com/events \
-H "Authorization: Bearer $AGENTISEND_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"example"}'202
{
"fired": -9007199254740991
}The first time a name arrives it is registered with the fields it carried, and it is not strict. GET /events is "Every custom event this account has declared, with its typed schema and when it was last seen." To make the shape a contract, use PATCH /events/{id} — "Declare or re-declare an event’s fields. Adding a field to a strict event starts refusing payloads that omit it — that is the point." schema maps each field to string, number, boolean, date or enum. With strict set to true, a field the schema does not declare is refused too, and the refusal names the field, so a typo becomes a refusal instead of an event nobody heard.
Steps
Each step has a kind:
email:from_addresson a verified domain,subject, andcontentin the shape templates use. The API's own sentence is "content must be {"mode": "markdown", "source": "..."} (mode markdown or html), or {"mode": "block", "blocks": [...]}." Placeholders such as{{first_name}}are filled from the contact's properties, andemailis always available. For stored, versioned bodies see Templates.webhook: a publicurl. The step POSTs JSON withtypeautomation.step,account_id, thecontact(idandemail) and the eventdata. A URL that points back at this API is skipped. The request is not signed.ab_split: avariantname andsplit.aandsplit.b, each its own list of steps. A contact lands in the same branch every time, decided by a stable hash of the contact id and the variant.
The graph
A graph automation is the body Resend's API takes: name, an optional status (enabled or disabled, and disabled when you leave it out), steps and connections, sent together.
{
"name": "Order follow-up",
"status": "enabled",
"steps": [
{ "key": "start", "type": "trigger", "config": { "event_name": "order.placed" } },
{
"key": "wait_for_payment",
"type": "wait_for_event",
"config": {
"event_name": "order.paid",
"timeout": "3 days",
"filter_rule": { "type": "rule", "field": "event.order_id", "operator": "eq", "value": { "var": "event.order_id" } }
}
},
{ "key": "receipt", "type": "send_email", "config": { "template": { "id": "order-receipt", "variables": { "ORDER": { "var": "event.order_id" } } } } },
{ "key": "nudge", "type": "send_email", "config": { "template": { "id": "payment-reminder" } } }
],
"connections": [
{ "from": "start", "to": "wait_for_payment" },
{ "from": "wait_for_payment", "to": "receipt", "type": "event_received" },
{ "from": "wait_for_payment", "to": "nudge", "type": "timeout" }
]
}The step types:
trigger:event_name, the event that starts a run. Exactly one, and nothing connects into it. Names that start withresend:are refused.delay:duration, a number and a unit, such as"30 minutes","12 hours","3 days"or"1 week". At least 1 second and at most 30 days.wait_for_event:event_name, an optionaltimeout(same units, at most 30 days) and an optionalfilter_rule. The run holds until that event arrives for the same contact. It leaves byevent_receivedor, when the timeout passes first, bytimeout. In afilter_rule,fieldreads the arriving event (event.) or the contact (contact.), and a{ "var": "event.x" }value reads the event that started the run. Later steps read what arrived aswait_events.<event name>.<field>.condition: a rule (field,operator,value) or anand/orgroup of rules. It leaves bycondition_metorcondition_not_met.fieldand{ "var": ... }values readevent.,contact.(contact.email,contact.first_name,contact.properties.plan) orwait_events.. The operators areeq,neq,gt,gte,lt,lte(numbers),contains,starts_with,ends_with(text),existsandis_empty. A value that reads data which is not there never matches.send_email: a publishedtemplateby id or alias, withvariablesthat are fixed values or{ "var": ... }references, and optionalfrom,subjectandreply_tothat replace the template's own.contact_update:first_name,last_name,unsubscribedandproperties, each a fixed value or a reference. A reference that finds nothing leaves that field as it was, and the run step lists it underunresolved.contact_delete: an emptyconfig. The contact is deleted and the rest of the run is skipped.add_to_segment:segment_id, one of your segments.
Every step except a condition and a wait leaves by default, which is what a connection without a type is. The graph is checked when you save it, and the refusal names the step or connection: every step reachable from the trigger, at most one connection of each type out of a step, no loops, no key used twice, at most 150 steps, and templates and segments that are yours. Enabling also needs every template to have a published version.
How a run moves. A run follows one path. It runs as far as it can when the event arrives, then holds at a delay or a wait; the run's state is saved with the run, so a restart of our servers does not lose a run or run a step twice. An email step for one run is sent at most once, even if the step is retried. A contact who is unsubscribed has their email steps skipped while the other steps still run. A step that fails stops the run, and the run is failed; a wait that times out with no timeout connection fails it too. A send held for review completes its step and records the hold.
Enabled means fixed. The steps and connections of an enabled automation cannot change; its name can. Disable it first, or POST /automations/{id}/duplicate — "Copy an automation, disabled, with " (copy)" after its name. Edit the copy, enable it, then stop the original."
Stop, delete. POST /automations/{id}/stop disables the automation: no new runs start, and runs already in progress finish as they started. Add ?cancel_runs=true to cancel those as well; a step that was waiting is marked skipped and nothing after it runs. DELETE /automations/{id} cancels any run still in progress, then deletes the automation, its versions and its runs.
Unsubscribe link and postal address
An email step is marketing mail, so it follows the same rules as a broadcast: it shows the reader how to stop getting it and who sent it.
- Postal address. The account's address, set once with
PATCH /accountand{"postal_address": "1 Main Street, Springfield, IL 62701"}or in the new broadcast form. Without one, an email step is not sent; the run records that step as failed and says where to set it. - Your own link. Put
{{unsubscribe_url}}where the link should go, for example[Stop these emails]({{unsubscribe_url}}). Each contact gets their own signed link. - The footer. If the step's body does not already show the link or the address, a short footer with both is added at the end, in the HTML and in the plain-text part. A contact who clicks it is unsubscribed, and later email steps skip them.
Enable, edit, disable
POST /automations/{id}/enable — "Snapshot trigger+steps as an immutable version and start firing." GET /automations/{id}/versions lists them, "Every immutable version, oldest first." An enabled automation cannot be edited: PATCH /automations/{id} answers "Disable the automation before editing its definition, or duplicate it and edit the copy; enabling snapshots a new immutable version." POST /automations/{id}/disable is "Stop firing. Versions stay for audit." Enabling again saves the next version, so a run always names the definition it executed.
curl -sS -X POST https://api.agentisend.com/automations/9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42/enable \
-H "Authorization: Bearer $AGENTISEND_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"200
{
"connections": [],
"created_at": "2026-09-04T09:14:00.000Z",
"current_version": -9007199254740991,
"format": "linear",
"id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
"name": "example",
"object": "automation",
"status": "example",
"steps": [],
"trigger": {
"event_name": "order.paid",
"type": "custom_event"
},
"updated_at": "2026-09-04T09:14:00.000Z"
}Did it run
GET /automations/{id}/runs lists runs newest first, each with status, trigger_type, event_name, contact_id, version_number, started_at and completed_at. A linear run is running, succeeded or failed; a graph run is running, completed, failed or cancelled. ?status= takes one status or a comma-separated list, and completed and succeeded find each other. A graph run that is holding shows current_step and waiting_for (the event, and when the delay or timeout ends). GET /automations/{id}/runs/{run_id} is "One run with every step: what it did, what it returned, and the sentence for the ones that failed." In a linear run each step has status succeeded, skipped or failed; an email step's output carries the message_id, and a refused send puts the refusal sentence in error. A failed step does not stop the steps after it, but it marks the run failed, and a split fails when a step in its branch does. In a graph run each step also has its key and type, status is completed, skipped, failed or waiting, an email step's output carries email_id, and error is { "message": ... }. Each POST /events also emits automation.triggered to your webhook endpoints, with the event name and fired.
curl -sS -X GET https://api.agentisend.com/automations/9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42/runs/9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42 \
-H "Authorization: Bearer $AGENTISEND_API_KEY"200
{
"automation_id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
"completed_at": "2026-09-04T09:14:00.000Z",
"contact_id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
"created_at": "2026-09-04T09:14:00.000Z",
"current_step": "example",
"event_name": "example",
"finished_at": "2026-09-04T09:14:00.000Z",
"id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
"object": "automation_run",
"started_at": "2026-09-04T09:14:00.000Z",
"status": "running",
"trigger_type": "example",
"version_number": -9007199254740991,
"waiting_for": {
"event": "example",
"until": "2026-09-04T09:14:00.000Z"
}
}