Provision with a background job
Think about when a team actually adds a member. Often it is the worst possible moment: something is
broken, and the one person who understands the failing subsystem is not in the workspace yet. The
admin who pages them should get an instant "done" — not sit on a request that is writing a
membership row, warming a cache, and sending a welcome email before it answers. Provisioning is real
work, and 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.
This is the same background-work seam a real NetScript app leans on — a production chat
application built on NetScript runs its embedding and vision jobs through the same workers plugin.
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 running. 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 install 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, 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 — console-backed logging
// progress(percent, message) — telemetry + runtime progress hook
// trace.addEvent / trace.withChildSpan — active-span event + real child span
//
// `id` is attached to the handler with Object.assign so the runtime
// registry can address the job by a stable string.
Step 3 — Scaffold the provision-member job
Start from the workers scaffold instead of creating the module by hand:
The workers CLI calls this the add-job verb; its shell syntax uses the spaced add job form:
ns-workers add job provision-member
The command writes workers/jobs/provision-member.ts with the stable export, payload-schema block,
and handler wrapper already in place, then refreshes the worker registry. Extend the generated
payload schema with workspaceId, subject, and role; import createFailureResult; add the
workspace Prisma import and client; then replace only the starter handler body with this application
logic:
const handler = defineJobHandler(async (ctx) => {
const parsed = PayloadSchema.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 });
});
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 — Confirm the generated registry
The Workers API addresses jobs by id, so the scaffold command refreshes the generated registry
before it returns. provision-member is already discoverable; do not run a second generation step.
Step 5 — Trigger the job
With Aspire up, the Workers API is live. Its host port was picked by the installer, so copy the
workers-api endpoint from the dashboard resource list. 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 is still a plain liveness probe.
curl <workers-endpoint>/health
# Inspect metadata, then enqueue through the durable workers API.
ns-workers show-job provision-member --json
ns-workers trigger provision-member \
--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:
ns-workers executions --limit=10 --json
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
https://localhost:18888 — the framework emits the dispatch/execution span
automatically.
| Name | Type | Description |
|---|---|---|
GET /health |
HTTP |
Liveness check for the Workers API service. |
ns-workers show-job {id} --json |
CLI |
Inspect local job metadata by id. |
ns-workers trigger {id} --payload=… |
CLI |
Enqueue an execution through the durable workers API. |
ns-workers executions --limit=10 --json |
CLI |
Recent executions and their result payloads. |
- [ ]
netscript plugin listshows theworkersplugin. - [ ]
plugins/workers/jobs/provision-member.tsexists and exports anid. - [ ]
ns-workers show-job provision-member --jsonshows its metadata. - [ ]
ns-workers triggerreturns quickly, andns-workers executionsshows it succeeded with amemberId. - [ ] 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 and observable in the Aspire dashboard. The caller gets an instant
acknowledgment; the membership write happens reliably in the background. One gap remains, and it is
the serious one: the workspace service itself still answers anyone who asks. Next you close the
loop and make its routes fail closed.