Skip to main content

background-tasks

This skill should be used when the user wants to "offload work to trigger.dev from next.js", "run a background job from a server action", "trigger a task from a route handler", "delegate slow work to trigger.dev", "decide what runs in a Server Action and what in a task", "show a task's progress in next.js", "give the browser a token for a trigger.dev run", "keep task environment variables apart from next.js", "fix a TRIGGER_SECRET_KEY build error", or needs the rules where a Next.js App Router app hands work to Trigger.dev and gets the result back.

Source facts

Repository
Agents-Store/opencode-plugins
Last source activity
October 3, 2026 at 23:16
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
background-tasks
description
This skill should be used when the user wants to "offload work to trigger.dev from next.js", "run a background job from a server action", "trigger a task from a route handler", "delegate slow work to trigger.dev", "decide what runs in a Server Action and what in a task", "show a task's progress in next.js", "give the browser a token for a trigger.dev run", "keep task environment variables apart from next.js", "fix a TRIGGER_SECRET_KEY build error", or needs the rules where a Next.js App Router app hands work to Trigger.dev and gets the result back.
# Background Tasks: The Next.js and Trigger.dev Boundary Slow, flaky or long work leaves Next.js and runs as a durable Trigger.dev task: it retries, it is observable, and it outlives the HTTP request. This skill owns the boundary only: what is offloaded, what crosses, who holds which credential, where the environment variables live. The SDK itself is taught by `trigger-dev`: starting runs from a route handler or Server Action, batches and idempotency in `task-development` (`references/triggering-patterns.md`); tokens and hooks for the browser in `realtime`; tasks, retries and queues in `task-development`. ## When to Offload Run work inline in a Server Action or Server Component when it finishes in under half a second, is deterministic and rarely fails, and the user is waiting for the result. Delegate to a task when any of these apply: | Signal | Why | |--------|-----| | A call to an LLM or AI API | Latency of 2 to 60 seconds, rate limits, retries needed | | Image, video or PDF processing | CPU heavy, likely longer than 5 seconds | | A third-party call that can fail (payment, email, webhook) | Retries and a trace per run come with the task | | A webhook receiver that does real work | Answer `200` fast and work asynchronously, so the sender does not retry | | A bulk operation over many records | Fan-out, with its own retry and status per record | | Work that must outlive the request (serverless limits are 10 to 60 seconds) | A task runs on its own workers | | Work on a schedule | `schedules.task` (`trigger-dev` → `scheduled-tasks`) | An HTTP call on a schedule with no logic of your own does not need a task: a Directus Flow with a Schedule trigger does it. ## What Crosses the Boundary | From → to | What travels | Rule | |-----------|--------------|------| | Next.js → task | Ids, and `requestedBy` for the audit trail | Never a token, never a copy of the item: the task reads the current record | | Next.js → browser | The run id, the **run-scoped** public token, `NEXT_PUBLIC_TRIGGER_API_URL` | Never `TRIGGER_SECRET_KEY`; a browser hook needs `baseURL` on self-hosted | | Task → Directus | Reads and writes with the **task user's** token | Not the Next.js server's token, not an administrator's | | Task → Next.js | `POST /api/revalidate` when needed (below) | Secret in the `x-revalidate-secret` header | | Directus Flow → Next.js → task | Item keys | `directus-to-trigger` | ## Start a Task from a Server Action The action authenticates and **authorizes** first, then starts the task with ids, then returns the run's handle. Authorizing by writing is the simplest check: the user's own token does a small write the policy must allow (here the status field), and Directus answers `403` when it does not: ```typescript // app/articles/actions.ts 'use server'; import { auth, tasks } from '@trigger.dev/sdk'; import { updateItem, withToken } from '@directus/sdk'; import directus from '@/lib/directus'; import { requireUser } from '@/lib/session'; import type { enrichArticle } from '@/trigger/enrich-article'; // a type import: no task code in the Next.js bundle export async function regenerateSummary(articleId: string) { const session = await requireUser(); // authenticate // Authorize: the user's own token marks the article pending; Directus applies that user's policy await directus.request( withToken(session.accessToken, updateItem('articles', articleId, { processing_status: 'pending' })), ); const handle = await tasks.trigger<typeof enrichArticle>( 'enrich-article', { itemId: articleId, requestedBy: session.user.id, force: true }, { tags: [`user-${session.user.id}`] }, // lets the browser's token be refreshed without a run id lookup ); return { runId: handle.id, publicAccessToken: handle.publicAccessToken }; } /** Called by the browser when its run token runs out. It can only read this user's own runs. */ export async function refreshRunToken(): Promise<string> { const session = await requireUser(); return auth.createPublicToken({ scopes: { read: { tags: [`user-${session.user.id}`] } }, expirationTime: '15m', }); } ``` - **Authorize before the trigger.** The task does not decide whether this user may do this: the action did. On the Better Auth path there is no user token, so check role or ownership in code before `tasks.trigger()`. - **Make the call inside the action or handler.** Importing the SDK reads no environment variable; the first call does. A build that fails with `You need to set the TRIGGER_SECRET_KEY environment variable` runs a trigger at module level or while prerendering: move the call into the function. A `POST` handler and a Server Action are never prerendered, so no `export const dynamic = 'force-dynamic'` is needed for them. - **`TRIGGER_API_URL` must be set where the call runs.** Without it the SDK falls back to Trigger.dev Cloud and sends your key there. - **A user-started task has a user; a Flow-started one does not.** `requestedBy` is for the trail, not for authorization (`authentication` → "Authenticated Tasks"). ## Show the Run in the Browser A Client Component takes the handle from the action and subscribes with `useRealtimeRun`. The code, the self-hosted `baseURL`, the token lifetime and `refreshAccessToken` (here: `refreshRunToken` above) are in `trigger-dev` → `realtime` → "From a Next.js App with Self-Hosted Trigger.dev". What the stack decides: - The only thing sent to the browser is the run's public token (read-only, one run, short-lived), plus the server address from `NEXT_PUBLIC_TRIGGER_API_URL`. - The action that mints a replacement token authorizes the caller first. A tag scope on the user's id, as above, makes that check structural: the token cannot read anyone else's runs. - Wrap handlers in an arrow function (`onClick={() => regenerateSummary(id)}`), or React passes the click event as the first argument. ## The Task Side - **Its own environment.** A task runs on the Trigger.dev workers and sees none of the Next.js host's variables. `DIRECTUS_URL` and `DIRECTUS_TOKEN` (the task user's token) are set in the Trigger.dev project per environment (`deployment` has the table), and in `.env.trigger.local` for `trigger dev`. - **Its own Directus client.** A task never imports `lib/directus.ts`: that module has `import 'server-only'`, which throws outside Next.js, and its token is the Next.js server's. The task-side client is in `directus-to-trigger`. - **Idempotent work.** A task is retried and replayed, and an editor saves the same item many times: it reads the record, skips what is done, and writes results that are safe to write twice. ## Closing the Loop: Fresh Pages After a Task Writes A task that writes Directus items changes what the pages show. Two ways to expire the tag, and the first needs no code in the task: 1. **The revalidation Flow does it** (default). It fires for every `items.update`, whoever made it, so a task's write to a collection the Flow lists expires the tag like an editor's save does. Add the collection to the Flow and to `COLLECTION_TAGS` of `/api/revalidate` (`deployment`; Step 6 of `full-feature`). 2. **The task calls `/api/revalidate` itself** (`revalidateSite()` in `directus-to-trigger`). Use it when the collection is not in the Flow, when the task changes data that never passes through Directus items, or when the site must not depend on a Flow that is easy to deactivate. It needs `NEXT_PUBLIC_SITE_URL` and `REVALIDATION_SECRET` in the Trigger.dev environment. A task that writes many items makes the Flow call the route once per write; that is fine for tens of items, and a reason for option 2 with batched writes for thousands. ## Anti-Patterns | Don't | Do | |-------|-----| | A token in a task payload | Ids and `requestedBy`; the task has its own token | | `await` a run's result in a user-facing Server Action | Return `runId` and the token, subscribe in the browser | | `import { myTask }` in Next.js code | `import type { myTask }` | | A trigger at module level | Trigger inside the handler or action | | `import 'server-only'` code in a task | A task-side client built from the task's environment | | Assume the task sees `.env.local` | Set the variables in the Trigger.dev project (deployed) or `.env.trigger.local` (dev) | | `TRIGGER_SECRET_KEY` in client code, or in a `NEXT_PUBLIC_` variable | The run's public token only | | A refresh action that mints a token for any run id it is given | Authorize first, or scope the token to the user's tag | | Retry a failed trigger in a loop inside the handler | Keep the handler thin; the task's `retry` does the retrying | ## Examples <example> User: "I need a Server Action that transcodes an uploaded video. Can I just await ffmpeg in the action?" Response: "Transcoding is exactly what to offload: ffmpeg can run for minutes and a serverless action is cut off after 10 to 60 seconds. Create a `transcode-video` task. The action calls `requireUser()`, checks that this user may touch the file (a small write with `withToken`, or a role check), starts the task with `{ fileId, requestedBy }` and returns `handle.id` and `handle.publicAccessToken`. Show progress in a Client Component with `useRealtimeRun`, passing `baseURL` from `NEXT_PUBLIC_TRIGGER_API_URL`. Import the task as a type, so ffmpeg stays out of the Next.js bundle, and give the task its own Directus token." </example> <example> User: "My CI build fails with 'You need to set the TRIGGER_SECRET_KEY environment variable' on a route that calls tasks.trigger()." Response: "Importing the SDK reads no variable; the first API call does. Something calls `tasks.trigger()` while Next.js builds: a trigger at module level, or in a route that is prerendered. Move the call into the handler or action body: a `POST` handler and a Server Action are never prerendered, so no `dynamic` export is needed. Do not put a key into the build environment to silence it." </example> <example> User: "After my task writes to Directus, the Next.js page still shows the old data." Response: "Check the chain in this order. Is the collection in the revalidation Flow and in `COLLECTION_TAGS` of `/api/revalidate`? Does the page's read carry that collection's tag (expanded relations included)? Is `REVALIDATION_SECRET` the same in the Directus container and the Next.js host, with `FLOWS_ENV_ALLOW_LIST` naming it? If the collection cannot be in the Flow, call the route from the task with `revalidateSite()` and check its response." </example>
View on GitHub