Skip to main content
0.0.x

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.

  1. 1 · Scaffold
  2. 2 · Auth
  3. 3 · Workspace data
  4. 4 · Provision job
  5. 5 · Route authz
  6. 6 · Deploy

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.

Workers API (endpoints used here)
NameTypeDescription
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 list shows the workers plugin.
  • [ ] plugins/workers/jobs/provision-member.ts exists and exports an id.
  • [ ] ns-workers show-job provision-member --json shows its metadata.
  • [ ] ns-workers trigger returns quickly, and ns-workers executions 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 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.