# Automations

> Create, manage and trigger automation flows via API.

Source: https://tickos.dev/docs/automations

Automations are step-based workflows (send email, wait, branch by condition, tag, change ticket status, webhook, etc.) that run over contacts enrolled when a trigger fires. An automation is composed of an automation record plus N steps (nodes) connected by next_step_id. The engine processes each enrollment step by step.

> **Plan required:** Automations are only available on Pro and Enterprise plans. Creating or activating returns 403 on other plans.

## Create flow

To build an automation from scratch: 1) POST /v1/automations creates the automation + trigger node. 2) PUT /v1/automations/{id}/steps defines the flow. 3) POST /v1/automations/{id}/activate validates and activates. 4) POST /v1/automations/{id}/enrollments enrolls contacts by email.

## GET /v1/automations -- List automations

Permission: read. Returns automations ordered by updated_at (most recent first). Query params: status (draft, active, paused, archived), limit (default 20, max 100) and offset.

```json
{
  "data": [
    {
      "id": "aut_xxxxxxxx",
      "name": "Onboarding API",
      "description": null,
      "status": "active",
      "trigger_type": "api",
      "trigger_config": {},
      "total_enrolled": 120,
      "total_completed": 98,
      "total_exited": 4,
      "created_at": "2026-07-01T10:00:00Z",
      "updated_at": "2026-07-02T09:00:00Z",
      "activated_at": "2026-07-02T09:00:00Z"
    }
  ],
  "pagination": { "limit": 20, "offset": 0, "total": 1 }
}
```

## GET /v1/automations/{id} -- Get automation details

Permission: read. Returns { data } with the automation (id, name, description, status, trigger_type, trigger_config, max_entries_per_contact, totals and dates) and all its steps (id, step_type, config, next_step_id, sort_order, created_at, updated_at) ordered by sort_order. Branch targets (true/false/timeout) are stored inside each step config. For editor positions use GET /v1/automations/{id}/steps.

## POST /v1/automations -- Create an automation

Permission: write. Plan: Pro/Enterprise. Creates the automation and its initial trigger node. Define the flow afterwards with PUT /v1/automations/{id}/steps.

| Field | Type | Required | Description |
|---|---|---|---|
| name | string | No | Automation name (default "Untitled Automation"). |
| description | string | No | Free-text description. |
| trigger_type | string | No | api (default) or any value from the trigger types table below. |
| trigger_config | object | No | Trigger filters, e.g. { "group_id": "grp_x" }. Also stored as the config of the initial trigger step. |
| from_email | string | No | Sender for send_email steps (domain must be verified). |
| from_name | string | No | Sender display name. |
| reply_to | string | No | Reply-to address. |
| exit_conditions | object | No | { on_unsubscribe, on_bounce, on_reply, on_booking }. |
| max_entries_per_contact | number | No | Default 1, max 10. |

```bash
curl -X POST https://api.tickos.dev/v1/automations \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Onboarding API", "trigger_type": "api" }'
```

Returns 201 with { data: automation }. New automations start in draft status.

### Trigger types

trigger_type must be one of the values below. trigger_config fields are filters: when omitted, the trigger matches any value. Send only these values; an unsupported trigger_type is rejected by the database.

| trigger_type | Fires when | trigger_config |
|---|---|---|
| api | POST /v1/automations/trigger is called for this automation. | {} |
| manual | A contact is enrolled manually from the dashboard. | {} |
| contact_added_to_group | A contact is added to a contact group. | { "group_id": "grp_x" } (required to activate) |
| tag_assigned | A tag is assigned to a ticket. | { "tag_id": "tag_x" } |
| ticket_status_changed | A ticket status changes. | { "from_status": "open", "to_status": "resolved" } |
| ticket_priority_changed | A ticket priority changes. | { "from_priority": "low", "to_priority": "high" } |
| ticket_agent_assigned | An agent is assigned to a ticket. | { "agent_id": "usr-x" } (optional) |
| email_event | Email open/click events (handled internally). | {} |
| broadcast_completed | A broadcast finishes sending. | { "campaign_id": "cmp_x" } (optional) |
| booking_scheduled | A meeting is booked (Scheduling). | { "calendar_id": "cal_x" } (optional) |
| booking_cancelled | A meeting is cancelled. | { "calendar_id": "cal_x", "cancelled_by": "invitee" \| "host" } (optional) |
| booking_rescheduled | The invitee reschedules a meeting. | { "calendar_id": "cal_x" } (optional) |

## PATCH /v1/automations/{id} -- Update an automation

Permission: write. Whitelisted fields: name, description, trigger_type, trigger_config, exit_conditions, max_entries_per_contact, from_email, from_name, reply_to. Other fields are ignored (use activate/pause to change status). Returns { data: automation }; 404 if the automation does not exist.

## DELETE /v1/automations/{id} -- Delete an automation

Permission: write. Deletes the automation and returns { "success": true }. Deleting an active automation returns 400; pause it first. 404 if it does not exist.

## GET /v1/automations/{id}/steps -- List steps

Permission: read. Returns { data: steps[] } ordered by sort_order. Each step includes id, step_type, config, next_step_id, position_x, position_y, sort_order, created_at and updated_at. Useful to read the current flow before sending a PUT.

## PUT /v1/automations/{id}/steps -- Save the flow

Permission: write. Bulk save of the whole flow. Body: { steps: [...], edges: [...] } (both arrays are required, otherwise 400). Each step has id, step_type, config, position_x, position_y and sort_order. Steps whose id starts with "new_" (or is unknown) are created; existing ids are updated; existing steps not included in the array are deleted. Then all next_step_id values are cleared and rebuilt from edges ({ source, target, label? }), which may reference either "new_" ids or real ids.

| Edge label | Written to the source step |
|---|---|
| (none) | next_step_id |
| "yes" or "true" | config.true_step_id |
| "no" or "false" | config.false_step_id |
| "timeout" | config.timeout_step_id AND config.false_step_id |

> **Timeout edges:** A "timeout" edge also sets false_step_id, so it acts as the "no" branch too. Do not combine a "timeout" edge with a separate "no"/"false" edge on the same step: whichever edge comes last in the array wins for false_step_id. Branch targets are kept in config across saves; to remove a branch, send the step config without it.

Response: { data: steps[], id_map }. data is the saved flow (same shape as GET /v1/automations/{id}/steps) and id_map maps each temporary "new_" id to the real step id, e.g. { "new_email": "ast_xxx" }. A step that fails to be created is skipped and is missing from id_map.

| step_type | config fields | Description |
|---|---|---|
| send_email | template_id OR (subject + content_html), content_text, preview_text, email_format, variables | Sends an email. Requires from_email or a verified domain. |
| wait_delay | duration, unit (minutes\|hours\|days\|weeks) | Waits a fixed time. |
| wait_condition | condition_type, condition_config, timeout_duration, timeout_unit, timeout_step_id | Waits until a condition is met or timeout expires. |
| condition | condition_type, condition_config, true_step_id, false_step_id | If/Else branch. |
| add_tag | tag_id | Adds the tag to the contact most recent open or pending ticket. Does nothing if there is none. |
| remove_tag | tag_id | Removes the tag from all of the contact tickets, whatever their status. |
| add_to_group / remove_from_group | group_id | On contact groups. |
| assign_agent | agent_id | Assigns the agent to all of the contact open and pending tickets. |
| change_ticket_status | status | Changes the status of all of the contact open and pending tickets. |
| change_ticket_priority | priority | Changes the priority of all of the contact open and pending tickets. |
| webhook | url, method, headers, custom_payload | Calls an external URL (10s timeout). method: GET, POST (default), PUT or PATCH. See Webhook step payload below. |
| start_automation | automation_id | Enrolls the contact into another automation. |
| exit | {} | Ends the enrollment. |

condition_type values: email_opened, email_clicked, email_replied, has_tag (needs condition_config.tag_id), in_group (needs condition_config.group_id). A condition step with a false result and no false_step_id ends the flow for that contact. A wait_condition that times out goes to timeout_step_id if set, otherwise to next_step_id.

```bash
curl -X PUT https://api.tickos.dev/v1/automations/aut_xxx/steps \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "steps": [
      { "id": "new_trigger", "step_type": "trigger", "config": {}, "position_x": 250, "position_y": 50, "sort_order": 0 },
      { "id": "new_email", "step_type": "send_email", "config": { "template_id": "tpl_x" }, "position_x": 250, "position_y": 180, "sort_order": 1 },
      { "id": "new_exit", "step_type": "exit", "config": {}, "position_x": 250, "position_y": 310, "sort_order": 2 }
    ],
    "edges": [
      { "source": "new_trigger", "target": "new_email" },
      { "source": "new_email", "target": "new_exit" }
    ]
  }'
```

### Webhook step payload

The webhook step sends the contact plus the full data of the event that triggered the automation. IDs stored when the contact was enrolled (ticket, tag, group, broadcast, agent, booking) are resolved to objects at send time. With POST, PUT or PATCH the payload is sent as a JSON body (Content-Type: application/json, User-Agent: TickOS-Automation/1.0, plus your custom headers). custom_payload keys are merged into the top level of the body and override fields with the same name.

```json
{
  "event": "automation.step.webhook",
  "trigger_event": "ticket.status_changed",
  "timestamp": "2026-10-03T12:00:00Z",
  "automation": { "id": "aut_x", "name": "Resolved follow-up", "trigger_type": "ticket_status_changed" },
  "enrollment": { "id": "aen_x", "enrolled_at": "2026-10-03T11:59:58Z" },
  "customer": { "id": "cst-x", "email": "ana@example.com", "name": "Ana", "phone": null, "metadata": {}, "source": "email", "created_at": "2026-09-01T08:00:00Z" },
  "trigger": {
    "type": "ticket_status_changed",
    "event": "ticket.status_changed",
    "ticket": {
      "id": "tck-x", "subject": "Cannot log in", "status": "resolved", "priority": "normal", "channel": "email",
      "metadata": {}, "created_at": "...", "updated_at": "...", "closed_at": null, "last_message_at": "...",
      "url": "https://app.tickos.dev/app/tickets/tck-x",
      "inbox": { "id": "inb_x", "name": "Support", "email_address": "support@acme.com", "channel_type": "email" },
      "assigned_to": { "id": "usr-x", "full_name": "John Smith", "email": "john@acme.com" },
      "tags": [{ "id": "tag_x", "name": "billing", "color": "#22c55e" }]
    },
    "changes": { "status": { "from": "open", "to": "resolved" } }
  },
  "automation_id": "aut_x",
  "automation_name": "Resolved follow-up",
  "enrollment_id": "aen_x",
  "step_id": "ast_x"
}
```

automation_id, automation_name, enrollment_id and step_id are flat legacy fields kept for compatibility. trigger.type is the trigger that enrolled the contact: contacts enrolled through POST /v1/automations/{id}/enrollments report type api (trigger_event contact.enrolled_via_api) whatever the automation trigger_type. Any extra keys from the enrollment context (for example the context you send when enrolling via API) are returned in trigger.context.

| Trigger | trigger_event | Data in trigger |
|---|---|---|
| contact_added_to_group | contact.added_to_group | group (id, name, description, color, total_contacts) |
| tag_assigned | ticket.tag_assigned | tag (id, name, color) + ticket |
| ticket_status_changed | ticket.status_changed | ticket + changes.status { from, to } |
| ticket_priority_changed | ticket.priority_changed | ticket + changes.priority { from, to } |
| ticket_agent_assigned | ticket.agent_assigned | ticket + agent (id, full_name, email) |
| email_event | email.event | Data available in the enrollment context. |
| broadcast_completed | broadcast.completed | broadcast (id, name, subject, status, from_email, from_name, send metrics, sending dates) |
| booking_scheduled | booking.scheduled | booking + calendar |
| booking_rescheduled | booking.rescheduled | booking (with previous_start_date, previous_start_time, previous_booking_id) + calendar |
| booking_cancelled | booking.cancelled | booking (with cancel_reason, cancelled_by) + calendar + cancelled_by |
| manual | contact.enrolled_manually | enrolled_by (id, full_name, email) + context |
| api | contact.enrolled_via_api | context with the data you sent |

booking includes id, status, calendar_id, invitee_name, invitee_email, invitee_phone, invitee_timezone, answers, start_at, end_at, ticket_id, rescheduled_from, cancel_reason, cancelled_by, cancelled_at, the dates already formatted in the invitee timezone (start_date, start_time, end_time, timezone, duration, location), manage_url, add_to_calendar_url and meeting_url. calendar includes id, name, slug, description, duration_minutes, location_type, location_value and timezone.

With method GET there is no body: only the main identifiers are sent as query params (event, trigger_event, trigger_type, automation_id, enrollment_id, customer_id, customer_email, customer_name, ticket_id, tag_id, tag_name, group_id, broadcast_id, booking_id, booking_start_at; empty values are omitted). Use POST to receive the full payload.

> **Delivery:** The webhook is sent once, with a 10 second timeout and no retries. A failed request or a non-2xx response is logged but does not stop the enrollment: the flow continues to the next step.

## POST /v1/automations/{id}/activate -- Activate

Permission: write. Plan: Pro/Enterprise. Sets status to active and returns { data: automation }. Contacts whose enrollments were paused by POST /v1/automations/{id}/pause resume.

| Status | When |
|---|---|
| 400 | The automation is already active. |
| 400 | It has no trigger step or no action step. |
| 400 | trigger_type is contact_added_to_group and trigger_config.group_id is missing. |
| 400 | The trigger step is not connected to a next step. |
| 400 | There is a send_email step, no from_email, and no verified sending domain. |
| 403 | The plan does not include automations. |
| 404 | Automation not found. |

## POST /v1/automations/{id}/pause -- Pause

Permission: write. Pauses the automation and its active enrollments (they resume when the automation is activated again). Returns { data: automation }. 400 if the automation is not active; 404 if it does not exist.

## POST /v1/automations/{id}/enrollments -- Enroll contacts by email

Permission: write. Plan: Pro/Enterprise. Enrolls one or more contacts into the automation, whatever its trigger type. The automation must be active. Contacts are looked up by email and created if they do not exist. Each contact starts at the first step right away. The re-enrollment limit (max_entries_per_contact) is respected.

| Field | Type | Required | Description |
|---|---|---|---|
| email | string | Yes* | Contact email. *Use either email or contacts. |
| name | string | No | Contact name (used when creating the contact). |
| context | object | No | Extra data stored on the enrollment (trigger_context). |
| contacts | array | Yes* | Bulk mode: up to 100 items, each an email string or { email, name?, context? }. A top-level context applies to items without their own. |

```bash
curl https://api.tickos.dev/v1/automations/aut_xxxxxxxx/enrollments \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{ "email": "jane@example.com", "name": "Jane", "context": { "plan": "pro" } }'
```

Single mode returns 201 with { data: { automation_id, email, status: "enrolled", customer_id, enrollment_id, customer_created } }. If the contact already has an active enrollment or reached the re-enrollment limit it returns 409 with { error, data: { automation_id, email, status, customer_id, error } }, where status is already_enrolled or max_entries_reached. An invalid email returns 400. The automation must be active (otherwise 400) and have at least one step after the trigger.

Bulk mode always returns 200 with a summary (total, enrolled, skipped, failed) and one result per email. skipped counts already_enrolled and max_entries_reached; failed counts invalid_email and error.

```json
{
  "data": {
    "automation_id": "aut_xxxxxxxx",
    "summary": { "total": 2, "enrolled": 1, "skipped": 1, "failed": 0 },
    "results": [
      { "email": "jane@example.com", "status": "enrolled", "customer_id": "cst-xxx", "enrollment_id": "aen_xxx", "customer_created": false },
      { "email": "john@example.com", "status": "already_enrolled", "customer_id": "cst-yyy" }
    ]
  }
}
```

## GET /v1/automations/{id}/enrollments -- List enrollments

Permission: read. Lists contacts enrolled in the automation, newest first (by enrolled_at). Query params: status (active, completed, exited, paused, failed; other values return 400), email (exact match, case-insensitive), limit (default 20, max 100) and offset.

```json
{
  "data": [
    {
      "id": "aen_xxx",
      "status": "active",
      "current_step_id": "ast_xxx",
      "enrolled_at": "2026-07-10T12:00:00Z",
      "completed_at": null,
      "exited_at": null,
      "exit_reason": null,
      "next_execute_at": "2026-07-11T12:00:00Z",
      "trigger_context": { "trigger_type": "api", "plan": "pro" },
      "customer": { "id": "cst-xxx", "email": "jane@example.com", "name": "Jane" }
    }
  ],
  "count": 1,
  "limit": 20,
  "offset": 0
}
```

## POST /v1/automations/trigger -- Trigger / enroll a contact

Permission: write. Fires the api trigger for the automation referenced by automation_id. The customer is looked up by exact email match and created (source api) if not found. The endpoint only checks that the automation exists (404) and is active (400); the contact is enrolled only if the automation trigger_type is api. Prefer POST /v1/automations/{id}/enrollments: it works with any trigger type, supports bulk and reports the result per contact.

> **Always 200:** Once the customer is resolved, the response is always 200 with { "success": true, "customer_id": "cst-xxx", "automation_id": "aut_xxx" }, even when nothing was enrolled: if the automation trigger_type is not api, the contact already has an active enrollment, it reached max_entries_per_contact, or the trigger is not connected to a next step, the call is skipped silently. Check GET /v1/automations/{id}/enrollments?email=... to confirm the enrollment.

| Field | Type | Required | Description |
|---|---|---|---|
| automation_id | string | Yes | ID of the automation to trigger (must be active). |
| customer_email | string | Yes | Customer email. Creates the customer if not found. |
| customer_name | string | No | Customer name (used when creating). |
| context | object | No | Additional context data passed to the enrollment. |

```bash
curl https://api.tickos.dev/v1/automations/trigger \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "automation_id": "aut_xxxxxxxx",
    "customer_email": "jane@example.com",
    "context": { "plan": "pro", "action": "upgrade" }
  }'
```
