Tasks
Tasks are operational entrypoints: backfills, repairs, imports, exports, and release maintenance that an operator runs on purpose. A task pairs a stable name with a validated input schema and a handler that receives the same app-owned context as the rest of the application, so operational work goes through ports, audit, and logging instead of one-off scripts.
Use a job when the app decides to run work in the background, a schedule when time triggers the work, and a task when a person, CI job, or release pipeline decides when it runs. See the background work overview for choosing between primitives.
Define a task
Create the app-bound builder once in lib/tasks.ts:
// lib/tasks.ts
import { createTasks } from "@beignet/core/tasks";
import type { AppContext } from "@/app-context";
export const { defineTask } = createTasks<AppContext>();Then define feature-owned tasks under features/<feature>/tasks/:
// features/issues/tasks/backfill-search.ts
import { z } from "zod";
import { defineTask } from "@/lib/tasks";
export const BackfillSearchInputSchema = z.object({
dryRun: z.boolean().default(true),
});
export const backfillSearchTask = defineTask("issues.backfill-search", {
input: BackfillSearchInputSchema,
description: "Backfill the issues search index.",
async handle({ input, ctx }) {
ctx.ports.logger.info("Task handled", {
taskName: "issues.backfill-search",
dryRun: input.dryRun,
});
return { dryRun: input.dryRun };
},
});Task handlers should call use cases and ports rather than reaching into infra
directly. Inputs are parsed with the task's schema before the handler runs, so
a typo'd flag fails with a TaskValidationError instead of mutating data.
The generator creates the task file, the lib/tasks.ts builder when it is
missing, and the registry entry in one step:
beignet make task issues/backfill-searchRegister tasks
server/tasks.ts owns central task registration and the operational context
used by the CLI:
// server/tasks.ts
import { createServiceActor } from "@beignet/core/ports";
import { defineTasks } from "@beignet/core/tasks";
import type { AppContext } from "@/app-context";
import { issueTasks } from "@/features/issues/tasks";
import { server } from "./index";
export const tasks = defineTasks([...issueTasks] as const);
export async function createTaskContext(): Promise<AppContext> {
return server.createServiceContext({
actor: createServiceActor("beignet-cli"),
});
}
export async function stopTaskContext(): Promise<void> {
await server.stop();
}The tasks export is the registry the CLI loads. createTaskContext builds
the app context for a run — a service actor plus the app's ports, providers,
audit, and devtools wiring — and stopTaskContext shuts providers down after
the run finishes. beignet make task keeps this registry updated for you.
Run a task
beignet task run issues.backfill-search --input '{"dryRun":true}'The CLI loads server/tasks.ts (override with --module), parses --input
as JSON against the task's schema, creates the context, runs the handler, and
prints the task name, duration, and output. Pass --json for machine-readable
output in CI or release jobs.
Testing
Run the task definition directly with runTask and a test context:
import { runTask } from "@beignet/core/tasks";
import { createTestContext } from "@beignet/core/testing";
import type { AppContext } from "@/app-context";
import { backfillSearchTask } from "@/features/issues/tasks";
const makeContext = createTestContext<AppContext>();
const fixture = makeContext();
const output = await runTask(backfillSearchTask, {
input: { dryRun: true },
ctx: fixture.ctx,
});
expect(output.dryRun).toBe(true);Memory ports make assertions cheap: capture logs and audit entries in memory,
then assert the task recorded what it did. Keep these tests in
features/<feature>/tests/.
Production
Tasks run from bounded entrypoints — a local shell, CI job, release job, or admin worker — so they need no exposed HTTP route. See Going to production for runtime entrypoints and operational auth.
Related pages
- Background work overview for choosing between primitives.
- Jobs for background work the app dispatches itself.
- Schedules for time-triggered work.
- Audit and activity logging for recording what operational work did.
- CLI for
beignet make taskandbeignet task runoptions.