Provision with a background job
Provisioning a new member is real work — write a membership row, maybe warm a cache, maybe send a
welcome email. None of it should block the request that triggered it. This chapter moves that work off
the request path: you add the workers plugin and author a defineJobHandler job that provisions a
member into the workspace database from chapter 3, then trigger it over the Workers API on :8091.
What you will build
A provision-member background job: a handler authored with defineJobHandler that parses a payload,
creates a Member in the workspace datasource, and returns a success result. By the end you trigger
it over the Workers API and watch its execution appear in the executions feed and its trace in the
Aspire dashboard.
Before you begin
You need the workspace database from chapter 3 with aspire startning. The workers plugin ships an API service and a background processor that Aspire orchestrates. Confirm the workspace datasource is ready:
# In my-workspace/, with `aspire start` up in another terminal
netscript db status --db workspace # the workspace datasource from chapter 3
Step 1 — Add the workers plugin
Add the workers plugin with its sample jobs so you have a working reference to read and adapt:
deno run -A packages/cli/bin/netscript-dev.ts plugin add worker --name workers --samples
netscript plugin list
The local-source contributor command lands the plugin at plugins/workers/ — the canonical, config-referenced install location —
and registers it in netscript.config.ts. On disk you get a jobs/ directory (the job-authoring
surface), a services/src/ API on :8091, and bin/combined.ts (the background processor
entrypoint).
Step 2 — Read the job-authoring API
A job is a function wrapped by defineJobHandler, given a stable id, and exported as the module
default. Inside the handler you receive a ctx, do the work, and return a result built with
createSuccessResult or createFailureResult. The sample plugins/workers/jobs/health-check.ts
shows the shape and the createJobTools(ctx) helper surface:
import {
createSuccessResult,
createFailureResult,
defineJobHandler,
} from '@netscript/plugin-workers-core';
import { createJobTools } from './job-tools.ts';
const handler = defineJobHandler(async (ctx) => {
const { log } = createJobTools(ctx);
log.info('doing work');
// ...do the work, return a result...
return createSuccessResult({ status: 'ok' });
});
export default Object.assign(handler, { id: 'my-job' as const });
// createJobTools(ctx) returns three helpers:
//
// log.info / log.warn / log.error — structured logging (REAL)
// progress(percent, message) — handler-side progress hook
// trace.addEvent / trace.withChildSpan
//
// `id` is attached to the handler with Object.assign so the runtime
// registry can address the job by a stable string.
Step 3 — Author the provision-member job
Add a new file in plugins/workers/jobs/ that provisions a member into the workspace datasource from
chapter 3. It parses its payload with Zod, writes a Member row, and returns a success result:
// plugins/workers/jobs/provision-member.ts
import {
createFailureResult,
createSuccessResult,
defineJobHandler,
} from '@netscript/plugin-workers-core';
import { z } from 'zod';
import { PrismaClient as WorkspacePrisma } from '../../../database/workspace/schema/.generated/client.server.ts';
const ProvisionMemberPayloadSchema = z.object({
workspaceId: z.string().min(1),
// The auth Principal.subject from chapter 2 — the user being provisioned.
subject: z.string().min(1),
role: z.string().default('member'),
});
const workspaceDb = new WorkspacePrisma();
const handler = defineJobHandler(async (ctx) => {
const parsed = ProvisionMemberPayloadSchema.safeParse(ctx.payload ?? {});
if (!parsed.success) {
return createFailureResult('invalid provision-member payload');
}
const { workspaceId, subject, role } = parsed.data;
const member = await workspaceDb.member.create({
data: { workspaceId, subject, role },
});
return createSuccessResult({ memberId: member.id, workspaceId, subject });
});
export default Object.assign(handler, { id: 'provision-member' as const });
This is the whole job — small on purpose. The membership write happens off the request path, so the caller that triggered provisioning never waits for it.
Step 4 — Generate the runtime registry
The Workers API addresses jobs by id, so it needs a generated registry mapping each id to its
handler. Generate the plugin registries so provision-member is discoverable:
netscript generate plugins
This scans plugins/workers/jobs and writes a registry the running service loads. After this,
provision-member is addressable over the API.
Step 5 — Trigger the job
With Aspire up, the Workers API is live on :8091. Confirm the service is healthy and the job is
registered, then trigger it by its id. You need a real workspaceId — create a Workspace row
first (or use one your seed created) and pass its id:
# Health, then confirm the job is registered
curl http://localhost:8091/health
curl http://localhost:8091/api/v1/workers/jobs # provision-member should appear
# Trigger it — the trigger enqueues an execution the background processor runs
curl -X POST http://localhost:8091/api/v1/workers/jobs/provision-member/trigger \
-H 'content-type: application/json' \
-d '{ "payload": { "workspaceId": "ws-1", "subject": "user:alice", "role": "member" } }'
Verify your progress
A trigger returns quickly because the work runs in the background. Confirm it actually executed by reading the executions feed:
curl 'http://localhost:8091/api/v1/workers/executions?limit=10'
You should see an execution record for provision-member with a succeeded status and a result payload
carrying the new memberId. Then watch the same run in the Aspire Traces view at
http://localhost:18888 — the framework emits the dispatch/execution span
automatically.
| Name | Type | Description |
|---|---|---|
GET /health |
HTTP |
Liveness check for the Workers API service. |
GET /api/v1/workers/jobs |
HTTP |
List registered job handlers by id — provision-member appears after generate. |
POST /api/v1/workers/jobs/{id}/trigger |
HTTP |
Enqueue an execution of a job by id. |
GET /api/v1/workers/executions?limit=10 |
HTTP |
Recent executions and their result payloads. |
- [ ]
netscript plugin listshows theworkersplugin. - [ ]
plugins/workers/jobs/provision-member.tsexists and exports anid. - [ ]
GET /api/v1/workers/jobslistsprovision-member. - [ ] Triggering it returns quickly, and the executions feed shows it succeeded with a
memberId. - [ ] The job-dispatch trace appears in the Aspire Traces view.
What you built
A provision-member background job that writes a workspace membership off the request path, triggered
over the Workers API on :8091 and observable in the Aspire dashboard. Provisioning no longer blocks
the caller. Next you close the loop: protect your service routes so only authenticated callers can
reach them.