# Background tasks

The four Trigger.dev tasks, how they are started from the Worker, and how the dashboard follows a run live.

Anything slow lives in background tasks and runs on Trigger.dev, not in the Worker. The Worker only starts tasks, through helpers.

## Configuration

`trigger.config.ts` sets the project from `TRIGGER_PROJECT_REF`, the task folder to `./trigger`, the Node runtime, a hard stop of 3,600 seconds and default retries (three attempts, exponential backoff, off in dev). AI tasks set `retry: { maxAttempts: 1 }` themselves.

## Tasks

### `sync-repo`

Payload `{ syncRunId }`. Reads the run and project, then reads the repository, plans, replaces any existing pages with the plan and writes them four at a time. It publishes progress to Trigger.dev metadata so the browser can follow it. A final step marks the project `draft_ready`. Started with an idempotency key `sync-run-<id>`, so a double submit cannot start two runs.

### `retry-page`

Payload `{ syncRunId, projectId, path }`. The API route first claims the page by setting it from `failed` to `pending`, creates a `page_retry` run, and starts the task. The task reads the repository at the project's last synced commit (or default branch if not set), writes the one page, and updates the page and the run. If the user edits the page by hand in the editor while the task runs, their manual edits win.

### `update-docs`

Payload `{ projectId }`. Configured with a queue `concurrencyLimit` of 1. The webhook starts it with:

```ts
tasks.trigger("update-docs", { projectId }, {
  debounce: { key: projectId, delay: "1m", maxDelay: "10m" },
  concurrencyKey: projectId,
});
```

Pushes less than a minute apart collapse into one run, and no run waits longer than ten minutes. The `concurrencyKey` makes one project's updates run one after another while different projects run in parallel. Starting throws if Trigger.dev is unreachable, which makes the webhook answer an error so GitHub can redeliver.

Before using any AI models, the task compares the last synced commit with the head of the default branch. If no documented code files changed, the task saves any updated file references (following renames and deletions) and succeeds immediately. If AI updates are required, the task checks the daily workspace limit of 15 updates per UTC day. If within the limit, it reads the repository, decides whether to extend existing pages or plan new draft pages (up to repository and project limits), and rewrites the affected pages three at a time. 

Unedited live pages are published directly, while edited pages receive a suggestion for review. If any changes are successfully published, the task evicts the cached site data and triggers the search index rebuild.

### `index-site`

Payload `{ projectId }`. Rebuilds the Ask AI index from the published pages. See [Ask AI internals](/development/ask-ai-internals). It is started after publish, unpublish, deleting a live page, and any update that put a page live.

Configured with a queue `concurrencyLimit` of 1. The helper starts it with:

```ts
tasks.trigger("index-site", { projectId }, {
  debounce: { key: projectId, delay: "10s", maxDelay: "1m" },
  concurrencyKey: projectId,
});
```

Publishes a few seconds apart collapse into one run, and the `concurrencyKey` makes sure one site is indexed by one run at a time. Starting it never throws: the pages are live either way, and the next publish tries again.

### `check-domains`

A scheduled task configured to run every ten minutes only in the production environment:

```ts
export const checkDomainsTask = schedules.task({
  id: "check-domains",
  cron: { pattern: "*/10 * * * *", environments: ["PRODUCTION"] },
  maxDuration: 300,
});
```

Keeps custom domain states in step with Cloudflare for SaaS. This ensures that a site switches to its custom domain as soon as its DNS starts working.

To avoid exceeding API rate limits, it processes up to 200 domains per run in parallel batches of 5. It selects domains that have an ID but either:
* Have no active timestamp and were added within the last 48 hours (while the user is setting up DNS).
* Have never been checked.
* Have not been checked in the last six hours.

When a domain transitions state, the task evicts the cached site data using `forgetSiteFromTask`. If there are domains to check but none can be successfully processed, it throws an error so the failure appears in the Trigger.dev dashboard.

## Starting a task safely

| Helper | Behavior on failure |
| --- | --- |
| `startSyncRun` | Marks the run failed with "We could not start writing your docs. Try again in a moment." The UI shows a retry button. |
| `startPageRetry` | Returns false. The API restores the page to failed. |
| `startIndexing` | Logs and continues. |
| `startDocsUpdate` | Throws, so GitHub redelivers. |

## Following a run live

`createRunToken` mints a public token with the scope `read: { runs: [triggerRunId] }` that expires after an hour. The run page uses `useRealtimeRun` from `@trigger.dev/react-hooks` with that token, so the browser can read that single run and nothing else.

The task reports a plain-data `SyncProgress` object with `metadata.set("progress", ...)`:

```ts
type SyncProgress = {
  step: "starting" | "reading" | "planning" | "writing" | "finishing";
  files: number;
  pages: { path: string; title: string; status: "pending" | "writing" | "done" | "failed" }[];
};
```

Browsers drop realtime connections in background tabs, so the view subscribes again when the tab becomes visible or the network comes back. When the run completes, the page refreshes to load the final state from the database. Without an active live connection (such as for an individual page retry, or when a page is still being retried after the sync run finishes), the dashboard polls the database by refreshing the page state every 4 seconds.

## Environment

Tasks read their own variables from the Trigger.dev environment and connect to Postgres directly. See [Environment variables](/development/environment-variables).
