# Automations

An **automation** joins two things: a `trigger_type` — the event that wakes it up — and an `action_kind` — what it does when that happens. It is the *when* of the agentic layer: [Functions and flows](functions-and-flows.html) holds the logic, [Tasks](tasks.html) holds the procedure, and this decides what sets them off.

Uses the `automations:read` and `automations:write` scopes, plus `automations:execute` to fire one on demand.

## The model in one sentence

**A type from the catalog, some conditions, and an action.** That's all:

```ts
const auto = await met.automations.create({
  name: 'Publish when the deal is won',
  trigger_type: 'item.field_changed',
  conditions: { collection_id: 42, field: 'estado', to: 'ganado' },
  action_kind: 'flow',
  flow_id: 'f1e2d3c4-0000-4000-8000-123456789abc',
});

console.log(auto.id, auto.enabled);   // → '…', true
```
```python
auto = met.automations.create(
    name="Publish when the deal is won",
    trigger_type="item.field_changed",
    conditions={"collection_id": 42, "field": "estado", "to": "ganado"},
    action_kind="flow",
    flow_id="f1e2d3c4-0000-4000-8000-123456789abc",
)

print(auto["id"], auto["enabled"])
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/workspaces/7/automations \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Publish when the deal is won",
        "trigger_type": "item.field_changed",
        "conditions": { "collection_id": 42, "field": "estado", "to": "ganado" },
        "action_kind": "flow",
        "flow_id": "f1e2d3c4-0000-4000-8000-123456789abc"
      }'
```

It is born enabled unless you send `enabled: false`. The optional fields are `description`, `icon`, `collection_id`, `action_config` and `enabled`.

## The catalog rules

`trigger_type` is not free text: it comes from the catalog. Read it before you build anything, because it is also the list of what you can offer your own user.

```ts
const catalog = await met.automations.catalog();
```
```python
catalog = met.automations.catalog()
```
```bash
curl https://api.met.meteor.com.co/api/v1/trigger-catalog \
  -H "Authorization: Bearer $MET_API_KEY"
```

Every entry carries `type` (what goes in `trigger_type`), `category`, `label`, `description`, `icon`, `conditions_schema` — what you must fill in —, `exposes` — the variables the action receives when it fires — and `enabled`.

> **`enabled: false` is not a missing permission.** Those are types published in the catalog that the dispatcher does not react to yet. You can read them; creating one answers `400`. Filter by `enabled` before drawing options in your own interface.

The types that fire today, with their required conditions:

| `trigger_type` | Fires when | Required conditions |
|---|---|---|
| `item.created` | A new item lands in a collection. | `collection_id` |
| `item.field_changed` | An item field moves to a specific value. | `collection_id`, `field`, `to` |
| `item.updated` | The item changes, whatever the field. | `collection_id` |
| `item.deleted` | An item is deleted from the collection. | `collection_id` |
| `contact.created` | A contact enters the CRM (by hand, through a channel, or over the API). | — |
| `contact.field_updated` | A contact field changes. | `field`, `operator` |
| `date_time` | An exact instant arrives. Once only. | `scheduled_date` (ISO 8601) |
| `recurrent` | The cron pattern comes around. | `recurrence_pattern` |
| `field_datetime` | A while before, at, or after a date field. | `source`, `field_id`, `direction` |
| `webhook.received` | A `POST` reaches one of your inbound webhooks. | `endpoint_slug` |
| `manual` | Only on request. See the note below. | — |

On create, only the **required** conditions are checked for presence; the rest of the object travels as is and each type interprets it. If one is missing, the response tells you which: `Condición "collection_id" requerida para trigger_type "item.created"`.

`recurrence_pattern` takes a five-part cron (`0 9 * * 1`) or one of these aliases: `hourly` (on the hour), `daily` (9:00), `weekly` (Monday 9:00) and `monthly` (1st of the month, 9:00). The pattern is evaluated in your workspace's time zone, and if it has none set, in Colombian time.

> **`manual` has no API trigger.** You can create it, but `run-now` covers managed processes and recurring flows only (see below). If you need to start something from your code whenever you decide to, run the flow directly with `met.flows.run(flowId)` or the task with `met.tasks.execute(taskId)`.

## What it runs: `action_kind`

| `action_kind` | What it does | What you must give it |
|---|---|---|
| `task` | Runs a task in the workspace. | `task_id` |
| `flow` | Runs a **published** flow. | `flow_id` |
| `ai_field` | Fills a collection field using AI. | `collection_id` and `action_config.field` |

If you omit `action_kind`, `task` is assumed — and then `task_id` becomes required.

> The flow has to be **published**. Wiring one that only has a draft answers `400`: publish it first with `met.flows.publish(flowId)` — see [Functions and flows](functions-and-flows.html). The task or flow also has to belong to your own workspace.

## Read, enable, disable and delete

```ts
const all = await met.automations.list();
const forOneTask = await met.automations.list({ taskId: 'd4c3b2a1-…' });

const one = await met.automations.retrieve(auto.id);

await met.automations.setEnabled(auto.id, false);              // turn it off
await met.automations.update(auto.id, { conditions: { collection_id: 42, field: 'estado', to: 'perdido' } });

await met.automations.remove(auto.id);
```
```python
all_of_them = met.automations.list()
for_one_task = met.automations.list(task_id="d4c3b2a1-…")

one = met.automations.retrieve(auto["id"])

met.automations.set_enabled(auto["id"], False)
met.automations.update(auto["id"], conditions={"collection_id": 42, "field": "estado", "to": "perdido"})

met.automations.remove(auto["id"])
```
```bash
curl https://api.met.meteor.com.co/api/v1/workspaces/7/automations \
  -H "Authorization: Bearer $MET_API_KEY"

curl -X PATCH https://api.met.meteor.com.co/api/v1/workspaces/7/automations/$AUTO_ID \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
```

`setEnabled()` is a shortcut for `update()`: it sends `enabled` alone and leaves the rest of the configuration untouched.

Two things the listing returns that are worth checking before you conclude something is broken:

- **`name` and `icon` always come resolved.** If you gave it no name, it falls back to the target task or flow, and finally to `(sin nombre)`; the icon falls back to the trigger type's own.
- **`task_name` and `flow_name`** save you the second call to find out what each row runs.

## When you are not the one who paused it

An automation can come back disabled without anyone touching it. It happens to the ones that run a flow: if a single automation runs away and eats the workspace's daily run ceiling on its own, Meteor **pauses that automation** and writes down why.

```ts
const a = await met.automations.retrieve(id);
if (!a.enabled && a.paused_reason) {
  // the system stopped it; a.paused_reason says why
}
```

`paused_reason: null` together with `enabled: false` means the opposite: a person turned it off. Switching it back on with `enabled: true` clears the reason and gives it a fresh count.

What matters for your integration: what gets paused is **the automation at fault, not the workspace**. The rest keep running.

## Managed processes

Some rows in the listing arrive with `is_system_managed: true`. Those are processes **Meteor publishes and versions** — reports, reminders, tools a Met uses inside a conversation. Their definition is not edited over `PATCH`, and `update()` with any field other than `enabled` answers `400`. They cannot be deleted either.

In exchange, they have a surface of their own. Before using it, look at what the row itself declares:

```ts
const p = await met.automations.retrieve(id);

p.is_system_managed;              // true
p.capabilities.editable_fields;   // ['schedule', 'flow']
p.capabilities.locked_fields;     // ['handler', 'tool_name', 'templates', 'recipients']
p.capabilities.can_restore;       // true
p.managed_revision;               // 2
p.manual_run;                     // does it accept running on demand?
p.last_run;                       // the latest run, already resolved
```

> The six methods that follow are **only** for these rows. On an automation you created they answer `400`, and that is the contract, not a problem with your key.

### Run history

```ts
const runs = await met.automations.runs(id, { limit: 50 });
// [{ id, source: 'scheduled' | 'manual', status, run_key, result, error_message, started_at, finished_at }, …]
```
```python
runs = met.automations.runs(automation_id, limit=50)
```
```bash
curl "https://api.met.meteor.com.co/api/v1/workspaces/7/automations/$AUTO_ID/runs?limit=50" \
  -H "Authorization: Bearer $MET_API_KEY"
```

Newest first. `limit` goes from 1 to 100; without it, 20. `run_key` is the key of the occurrence (for example `2026-08-14`): it is what keeps the schedule from running the same thing twice.

For the history of one of your own flows the route is a different one: `met.flows.listRuns(flowId)`.

### Run on demand

```ts
const { ok, already_processed, run } = await met.automations.runNow(id, {
  input: { periodo: '2026-08-14' },
});
```
```python
res = met.automations.run_now(automation_id, input={"periodo": "2026-08-14"})
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/workspaces/7/automations/$AUTO_ID/run-now \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":{"periodo":"2026-08-14"}}'
```

`already_processed: true` means that occurrence had already run and your request duplicated nothing.

**The response has two shapes, and mixing them up costs an afternoon:**

- If the process runs a system job (`action_kind: 'system_job'`), `run` is the real run: the same `id` you will later see in `runs()`.
- If it runs a flow (`action_kind: 'flow'`), `run` comes back with `status: 'processing'` and an `id` that belongs to the **flow run**, not to a managed run. That `id` never shows up in `runs()`. To follow it:

```ts
const flowRun = await met.flows.findRun(run.id);
```

It answers `400` if the process is paused, if it does not accept running on demand (`manual_run` is `false`), or if it is a flow that is not recurring.

### Change the schedule

```ts
await met.automations.updateSchedule(id, '0 12 * * 1-6');   // 12:00, Monday to Saturday
```
```python
met.automations.update_schedule(automation_id, "0 12 * * 1-6")
```
```bash
curl -X PATCH https://api.met.meteor.com.co/api/v1/workspaces/7/automations/$AUTO_ID/schedule \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"recurrence_pattern":"0 12 * * 1-6"}'
```

The cron is **deliberately narrow**: five parts, with a fixed hour and days of the week. Day of month and month must be `*`; anything else answers `400`. And it only applies if `capabilities.editable_fields` includes `schedule` — a process that fires on an event has no schedule to change.

### Edit without overwriting anyone

```ts
const p = await met.automations.retrieve(id);

await met.automations.updateConfiguration(id, {
  expected_revision: p.managed_revision,
  recurrence_pattern: '0 7 * * 1-5',
  flow_id: null,                 // disconnects the follow-up flow
});
```
```python
p = met.automations.retrieve(automation_id)

met.automations.update_configuration(
    automation_id,
    expected_revision=p["managed_revision"],
    recurrence_pattern="0 7 * * 1-5",
    flow_id=None,
)
```
```bash
curl -X PATCH https://api.met.meteor.com.co/api/v1/workspaces/7/automations/$AUTO_ID/configuration \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expected_revision":2,"recurrence_pattern":"0 7 * * 1-5","flow_id":null}'
```

`expected_revision` is the revision the row carried when you read it. If someone changed it in between, the call fails with **`409`** instead of overwriting their change; the SDKs surface it as `MetInvalidRequestError` with `status` 409. Re-read the process and try again with the new revision.

Send at least one of the two configurable fields: with neither, it answers `400`. `flow_id: null` is explicit and is sent — it disconnects the flow.

### Undo, and see who changed what

```ts
await met.automations.restoreConfiguration(id, { expected_revision: p.managed_revision });

const changes = await met.automations.configurationHistory(id, { limit: 20 });
// [{ revision, operation: 'update' | 'restore', changed_by_user_id, before_config, after_config, created_at }, …]
```
```python
met.automations.restore_configuration(automation_id, expected_revision=p["managed_revision"])

changes = met.automations.configuration_history(automation_id, limit=20)
```

`restoreConfiguration()` returns the process to the configuration Meteor published, and only applies if `capabilities.can_restore` is `true`. The history goes newest first, with `limit` between 1 and 100 (without it, 20).

## Scheduling a task: use this API, not the task's own

There is an older route for hanging a trigger off a task from the task itself (`POST /tasks/{taskId}/triggers`). It is still published for compatibility, but what it creates **is not tied to a workspace**, so it does not show up in `GET /workspaces/{workspaceId}/automations` and it does not go through the catalog's validation.

To schedule a task today, create an automation that points at it:

```ts
await met.automations.create({
  name: 'Qualify leads every Monday',
  trigger_type: 'recurrent',
  conditions: { recurrence_pattern: '0 9 * * 1' },
  action_kind: 'task',
  task_id: task.id,
});
```

## Errors you will see

| Response | What causes it |
|---|---|
| `400` `trigger_type "…" no existe en el catálogo` | The type is not in `GET /trigger-catalog`. |
| `400` `…todavía no está habilitado en producción` | The type exists but came back with `enabled: false`. |
| `400` `Condición "…" requerida para trigger_type "…"` | A required condition is missing. |
| `400` `El flow no ha sido publicado` | You wired a flow that only has a draft. |
| `400` `La task no pertenece a este workspace` | The task or flow belongs to another workspace. |
| `400` `Esta automatización no tiene historial administrado` | You called `runs()` on an automation of your own. |
| `400` `Los procesos administrados solo permiten activar o pausar` | An `update()` with fields other than `enabled` on a managed process. |
| `409` `La configuración cambió en otra sesión` | Your `expected_revision` went stale. |

The rest — `401` for a wrong key or workspace, `429` for limits — behaves like everywhere else in the API: see [Errors, idempotency and pagination](errors-and-idempotency.html).
