> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deck.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate task runs

> Schedule tasks across your credentials with fan-out, skip logic, and cost controls.

Run a task on a schedule across your connected credentials. You define a [trigger](/concepts/triggers) once, and Deck creates and manages the runs from there.

A trigger pairs a schedule with a [task](/concepts/tasks) and a set of [credentials](/concepts/credentials). Each time the schedule fires, Deck creates one [task run](/concepts/task-runs) per credential in scope, skipping any that don't need the work. Triggered runs behave exactly like runs you create with `POST /v2/tasks/{task_id}/run`: same lifecycle, statuses, artifacts, and events.

## Example

Fetch the latest bill for every connected utility account, every day at 6am Eastern:

```text theme={null}
POST /v2/triggers
```

```json theme={null}
{
  "name": "Daily utility bills",
  "condition": {
    "type": "schedule",
    "cron": "0 6 * * *",
    "timezone": "America/New_York"
  },
  "task_id": "task_a1b2c3...",
  "credential_filter": {
    "source_ids": ["src_utility..."],
    "status": ["unverified", "verified"]
  },
  "input": {},
  "skip_if": {
    "last_run": {
      "result": "success",
      "within_seconds": 2160000
    }
  },
  "concurrency_max": 50
}
```

Every day at 6am, Deck resolves the credentials matching the filter, skips any whose latest run of this task succeeded within the past 25 days (2,160,000 seconds), and creates a task run for each of the rest.

### Definition fields

| Field               | Required    | Meaning                                                                                      |
| ------------------- | ----------- | -------------------------------------------------------------------------------------------- |
| `name`              | Yes         | Display name for the trigger                                                                 |
| `description`       | No          | Optional description                                                                         |
| `condition`         | Yes         | When the trigger fires. See [The schedule](#the-schedule).                                   |
| `task_id`           | Yes         | The task to run                                                                              |
| `credential_ids`    | Conditional | Explicit credentials, one or many. Exactly one of `credential_ids` / `credential_filter`.    |
| `credential_filter` | Conditional | A scope of credentials to fan out across. See [Choosing credentials](#choosing-credentials). |
| `input`             | No          | Input passed to every run, validated against the task's `input_schema`                       |
| `skip_if`           | No          | Skip condition evaluated per credential. See [Skip conditions](#skip-conditions).            |
| `concurrency_max`   | No          | Max simultaneous runs from this trigger. `null` means no per-trigger cap.                    |

The response is the full trigger object (`trg_` prefix) with `status: "active"`. Triggers fire on their schedule from the moment they're created.

```json theme={null}
{
  "id": "trg_a1b2c3...",
  "object": "trigger",
  "name": "Daily utility bills",
  "status": "active",
  "condition": {
    "type": "schedule",
    "cron": "0 6 * * *",
    "timezone": "America/New_York"
  },
  "task_id": "task_a1b2c3...",
  "credential_ids": null,
  "credential_filter": {
    "source_ids": ["src_utility..."],
    "status": ["unverified", "verified"]
  },
  "input": {},
  "skip_if": {
    "last_run": {
      "result": "success",
      "within_seconds": 2160000
    }
  },
  "concurrency_max": 50,
  "created_at": "2026-06-18T00:00:00Z",
  "updated_at": "2026-06-18T00:00:00Z"
}
```

## The schedule

`condition` defines when the trigger fires: a cron expression evaluated in a timezone.

```json theme={null}
{
  "type": "schedule",
  "cron": "0 6 * * *",
  "timezone": "America/New_York"
}
```

| Field      | Meaning                                                                                                                                      |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`     | Always `schedule`                                                                                                                            |
| `cron`     | Standard 5-field cron expression: minute, hour, day of month, month, day of week                                                             |
| `timezone` | IANA timezone name (e.g. `America/New_York`). Occurrences are computed in this timezone, so day boundaries and DST shifts follow local time. |

An invalid cron expression or timezone is rejected at create time with a standard [validation error](/api/errors).

Missed occurrences are skipped, not replayed. If a trigger sits `inactive` across several scheduled times, reactivating it resumes at the next future occurrence; Deck never back-fills fires it missed.

## Choosing credentials

Every trigger targets credentials one of two ways. Pass exactly one of `credential_ids` / `credential_filter`.

### An explicit list

```json theme={null}
{
  "credential_ids": ["cred_abc...", "cred_xyz..."]
}
```

The set is static: exactly the credentials you list, one or many, until you change it with a PATCH. There's no status allowlist because you've named them directly. Credentials in the list that are `invalid` or `deleted` when the trigger fires are skipped automatically; you don't need to prune the list.

### A filter

```json theme={null}
{
  "credential_filter": {
    "source_ids": ["src_utility..."],
    "status": ["unverified", "verified"]
  }
}
```

Deck resolves the filter each time the trigger fires and creates a run for every match, so enrollment is fully dynamic. A credential created tomorrow that matches the filter starts getting runs on the next fire, with no registration step.

Both keys are lists and both are required:

* `source_ids` matches credentials on any of the listed [sources](/concepts/sources), or on every source with the `["*"]` wildcard.
* `status` is an explicit allowlist of [credential statuses](/concepts/credentials#credential-statuses) to fan out to, typically `unverified` and `verified`. There is no default. `invalid` and `deleted` credentials can't run, so they never match.

A credential's open or closed session state is independent of its status. A credential at rest still matches the filter, and the run authenticates a fresh [session](/concepts/sessions) as needed.

A filter that can never match anything (an empty `status`, or an empty `source_ids`) is rejected when you create or update the trigger with a standard [validation error](/api/errors), rather than accepted as a trigger that fires and creates nothing.

### Invalid credentials

When a credential goes `invalid` (a changed password, a locked account), Deck emits a `credential.invalid` [event](/events/events) so you can prompt the user to re-authenticate. Under a filter, the credential drops out of the match entirely; under an explicit list, it stays listed but is skipped. Either way the trigger keeps running for every healthy credential, and once the user re-authenticates, runs resume automatically on the next fire.

The trigger doesn't track credential failures itself; a credential's status determines whether it runs.

## Skip conditions

`skip_if` is evaluated per credential each time the trigger fires, before a run is created. When it's met, the credential is skipped this time and no run is created.

```json theme={null}
{
  "skip_if": {
    "last_run": {
      "result": "success",
      "within_seconds": 2160000
    }
  }
}
```

`last_run` looks at the credential's most recent completed run of the trigger's task, whether that run came from this trigger or from a direct API call. If its `result` matches within the window, the credential is skipped.

`result` is the task run's [result](/concepts/task-runs) field: `success`, `failure`, or `unknown`.

### Design the task so skipping self-heals

The utility bill case works when the task's success criteria mean "did the work": a run that fetches a bill returns `result: success`, and a run that finds no new bill completes with `result: unknown`. After a bill is fetched, the credential is skipped for 25 days, and checking resumes once the window lapses, in time for the next bill. This comes from how the task defines success rather than from any trigger setting. If a no-bill run reported `success`, it would suppress the checks that should follow.

### Using skip conditions to control cost

Every run executes in a real agent [session](/concepts/sessions). 10,000 credentials on a daily schedule is roughly 300,000 runs a month without skipping. With a 25-day success window, each credential only runs on the few checking days before its next bill lands, roughly 50,000 runs a month. Pick a window slightly shorter than the real cadence of the data.

## Inputs under fan-out

<Warning>
  A trigger has a single `input`, shared by every run it creates. There is no per-credential input. If each user's run needs a different value from your system (a target date, a confirmation number), a trigger can't supply it: create those runs from your own code with `POST /v2/tasks/{task_id}/run`.
</Warning>

The trigger's `input` is validated against the task's `input_schema`, the same shape as the `POST /v2/tasks/{task_id}/run` body. Fields the task declares as tokenized are vaulted when you create the trigger, exactly as they are at run time.

Per-user differentiation comes from the credential, not the input. Each run executes in that credential's authenticated session, so a task like "fetch the latest bill" runs with `input: {}` and fetches the right bill for whoever's logged in. Design tasks you plan to schedule this way: operate from the logged-in context and discover what's needed on the source, rather than expecting per-user parameters.

## Managing triggers

Creating, listing, updating, and deleting triggers is covered endpoint by endpoint in the [API reference](/api-reference/triggers/list-triggers). For day-to-day management (pausing a schedule, editing a filter, checking what's enrolled, reviewing recent fires), the [Console](https://console.deck.co) is usually the quicker path: each task's detail page has a Triggers section with everything the API offers.

The semantics that aren't obvious from the endpoint docs:

* Updates take any subset of fields, and omitted fields are left unchanged. An explicit `null` clears `description`, `skip_if`, or `concurrency_max`, and `credential_ids` replaces the existing list wholesale.
* Pause and resume by setting `status` to `inactive` or `active`; there are no separate endpoints. An `inactive` trigger keeps its configuration and fires nothing, and reactivating resumes at the next scheduled occurrence.
* A trigger's target can't be changed. To point at a different task, create a new trigger.

To see which credentials are currently in scope (the set that gets runs on the next fire), use [`GET /v2/triggers/{trigger_id}/credentials`](/api-reference/triggers/list-trigger-credentials) or the Console's enrollment view. In the other direction, a credential's `GET` response lists the triggers targeting it.

## Tracing runs to a trigger

Triggered runs are normal task runs with one addition: a `trigger_id` field, always present and `null` unless the run was created by a trigger. Filter runs by it to inspect a single fire or audit a trigger's history, combined with the existing task run filters:

```text theme={null}
GET /v2/task-runs?trigger_id=trg_a1b2c3...&outcome=failure
```

## Queueing and concurrency

Triggered runs don't all execute at the same instant. Each run needs an agent session, and runs are admitted as capacity frees up.

* **Per trigger.** `concurrency_max` caps simultaneous runs from one trigger. A fan-out matching 2,000 credentials with `concurrency_max: 50` runs 50 at a time, with the rest queued.
* **Per organization.** Scheduled runs draw on their own capacity pool, separate from on-demand runs, and on-demand runs are admitted first. A large overnight fan-out never delays a user-facing `POST /v2/tasks/{task_id}/run`.

You don't manage `session_limit_exceeded` for scheduled work. Deck queues internally and admits runs as slots free up.

### Overlapping fires

If a trigger fires again while the previous fire's runs are still draining, Deck coalesces: the newer fire supersedes any still-queued run for the same credential, so the freshest scheduled work wins. Runs that are already executing are left alone.

### Exactly-once per occurrence

Each fire is keyed by the trigger and its scheduled time, so a duplicate delivery of the same occurrence never creates a second batch of runs.

## Failure handling

A failed triggered run is a normal task run: queryable by `trigger_id` and status, carrying the standard [errors](/api/errors) array. What happens next depends on the error class:

* **Transient** (`source_not_available`, `blocked`, `timeout`): nothing special. The credential simply runs again on the next fire.
* **Credential** (`auth_invalid`, `password_reset_required`, `account_locked`): the agent marks the credential `invalid` and it leaves the fan-out on its own, resuming once the user re-authenticates. See [Invalid credentials](#invalid-credentials).
* **Configuration** (a deleted task, input that no longer validates): these fail the fire itself, surfaced as a `trigger_run.failed` event rather than failing silently.

### Auto-deactivate

If a trigger's fires keep failing (default threshold: 5), it transitions to `inactive` and emits `trigger.deactivated` with the reason, so a misconfigured trigger doesn't keep failing on schedule indefinitely. This deactivates the whole trigger, never individual credentials. A deactivated trigger shows on its task's detail page in the [Console](https://console.deck.co) along with the failed fires that got it there. Once the configuration is fixed, reactivate it from the same page or with `PATCH { "status": "active" }`.

## Events

Triggers emit into the existing [events](/events/events) system on two channels.

Lifecycle events track the configuration object:

| Event                 | Fired when                                                                 |
| --------------------- | -------------------------------------------------------------------------- |
| `trigger.created`     | A trigger is created                                                       |
| `trigger.updated`     | A trigger's configuration changes                                          |
| `trigger.deleted`     | A trigger is deleted                                                       |
| `trigger.activated`   | A trigger becomes active (on create, or via PATCH)                         |
| `trigger.deactivated` | A trigger becomes inactive, manually or automatically. Carries the reason. |

Trigger run events track each fire. A trigger run (`tgrn_` prefix) is one scheduled execution: the fan-out across credentials that creates the underlying task runs.

| Event                   | Fired when                                                                    |
| ----------------------- | ----------------------------------------------------------------------------- |
| `trigger_run.completed` | A fire completed. Carries the count of runs created and credentials skipped.  |
| `trigger_run.failed`    | A fire failed before creating any runs (e.g. the referenced task was deleted) |

```json theme={null}
{
  "id": "evt_a1b2c3d4",
  "object": "event",
  "type": "trigger_run.completed",
  "data": {
    "id": "tgrn_a1b2c3d4",
    "object": "trigger_run",
    "trigger_id": "trg_a1b2c3...",
    "status": "completed",
    "runs_created": 412,
    "skipped_count": 9588,
    "created_at": "2026-06-23T06:00:02Z"
  },
  "created_at": "2026-06-23T06:00:02Z"
}
```

The task runs a fire creates emit their own standard `task_run.*` events, tagged with `trigger_id`, so existing handlers work for scheduled runs without changes.
