Skip to main content

Workers quickstart

Altstack Workers defines typed jobs: named units of work with Zod payloads, middleware, and application context. A server adapter materializes the router as Trigger.dev tasks or a WarpStream/Kafka consumer. This differs from the Kafka family, where procedures react to domain events on topics.

This quickstart uses Trigger.dev first because it implements on-demand tasks and cron schedules. A WarpStream alternative follows.

1. Install the Trigger.dev adapter

pnpm add @alt-stack/workers-trigger @trigger.dev/sdk zod

You need Node.js 18 or newer, Zod 4, the Trigger.dev SDK 3.x, and a configured Trigger.dev project. Initialize/configure Trigger.dev using its CLI before running trigger dev.

@alt-stack/workers-trigger re-exports the entire workers-core API, so one import can define the router and create its runtime tasks.

2. Define a job router

src/jobs.ts
import { init, ok } from "@alt-stack/workers-trigger";
import { z } from "zod";

export interface AppContext {
mailer: {
sendWelcome(input: { email: string; name: string }): Promise<void>;
};
}

const { router, procedure } = init<AppContext>();

export const jobs = router({
"send-welcome-email": procedure
.input({
payload: z.object({
email: z.string().email(),
name: z.string(),
}),
})
.task(async ({ input, ctx }) => {
await ctx.mailer.sendWelcome(input);
return ok();
}),

"daily-digest": procedure.cron("0 9 * * *", async ({ ctx }) => {
// Build and send the daily digest.
return ok();
}),
});

The router key is the job ID. .task() creates an on-demand job; .cron() records a schedule; .queue() records queue metadata. Payloads are validated before custom context is created or the handler runs.

Handlers are typed to return an Altstack Result; ok() is the successful no-value result. In the current adapters, a returned Err is still a normal handler return and is not converted into a provider failure. Throw an Error when the provider must mark the run failed or apply its retry policy.

3. Export Trigger.dev tasks

src/trigger/tasks.ts
import { createWorker } from "@alt-stack/workers-trigger";
import { jobs, type AppContext } from "../jobs.js";

const mailer: AppContext["mailer"] = {
async sendWelcome({ email, name }) {
console.info(`Welcome ${name} -> ${email}`);
},
};

export const { tasks } = createWorker(jobs, {
createContext: () => ({ mailer }),
onError: (error, ctx) => {
console.error(`${ctx.jobName}:${ctx.jobId} failed`, error);
},
});

export const sendWelcomeEmail = tasks["send-welcome-email"];
export const dailyDigest = tasks["daily-digest"];

Place the file under the task source directory configured for Trigger.dev. createWorker is synchronous and returns a record of Trigger.dev task definitions. Export the definitions you want Trigger.dev to discover.

Start the Trigger.dev development process with the configured project command, for example:

pnpm exec trigger dev

4. Trigger the task

From application code using Trigger.dev directly:

import { tasks } from "@trigger.dev/sdk/v3";

async function main() {
const handle = await tasks.trigger(
"send-welcome-email",
{ email: "ada@example.com", name: "Ada" },
);

console.info(handle.id);
}

void main();

This direct Trigger.dev call uses a string task ID, so its payload is not statically tied to the router. For a type-safe caller, generate a worker SDK and pass its runtime Topics map to createTriggerClient from @alt-stack/workers-client-trigger. That client validates payloads without importing the server router.

WarpStream/Kafka alternative

Install @alt-stack/workers-warpstream, KafkaJS, and Zod. You need a reachable Kafka-compatible broker and must provision the job topics. Default routing uses one topic per job and concatenates an optional prefix directly with the job name.

import { createWorker } from "@alt-stack/workers-warpstream";
import { jobs } from "./jobs.js";

const worker = await createWorker(jobs, {
kafka: { brokers: ["localhost:9092"] },
groupId: "email-workers-v1",
createContext: () => ({ mailer }),
});

const shutdown = () => worker.disconnect();
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);

Enqueue from code that can import the router:

import { createJobClient } from "@alt-stack/workers-warpstream";

const client = await createJobClient(jobs, {
kafka: { brokers: ["localhost:9092"] },
});

await client.enqueue("send-welcome-email", {
email: "ada@example.com",
name: "Ada",
});

await client.disconnect();

createWorker and createJobClient must receive the same routing strategy. The generated-schema @alt-stack/workers-client-warpstream binding supports only topic-per-job routing.

Server adapters versus clients

BoundaryPackageInput contractWhat it does
definitionworkers-coreWorkerRouterdefines jobs; no provider connection
serverworkers-triggerWorkerRoutercreates Trigger.dev task definitions
serverworkers-warpstreamWorkerRouterstarts a KafkaJS consumer and executes jobs
co-located clientworkers-warpstream#createJobClientlive WorkerRouterenqueues with router-derived types
generated clientworkers-client-triggergenerated Zod JobsMaptriggers through Trigger.dev
generated clientworkers-client-warpstreamgenerated Zod JobsMappublishes topic-per-job messages

Next steps