# 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](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/01-scaffold/)
2. [2 · Auth](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/02-auth/)
3. [3 · Workspace data](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/03-workspace-data/)
4. [4 · Provision job](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/04-provision-job/)
5. [5 · Route authz](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/05-route-authz/)
6. [6 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/06-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](https://rickylabs.github.io/netscript/tutorials/workspace/03-workspace-data/) with **Aspire running**. The workers plugin ships an API service and a background processor that Aspire orchestrates. Confirm the workspace datasource is ready:

```sh
# In my-workspace/, with `aspire start` up in another terminal
netscript db status --db workspace   # the workspace datasource from chapter 3
```

> Aspire first, then everything else
>
> The Workers API and its background processor are resources in the Aspire graph, and so is the
>
> dashboard on `:18888`
>
> where you read job traces. Start
>
> aspire start
>
> from
>
> aspire/
>
> before
>
> you add the plugin or trigger a job, and leave it running.

## Step 1 — Add the workers plugin

Add the workers plugin with its sample jobs so you have a working reference to read and adapt:

```sh
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).

> Author jobs in plugins/workers/
>
> A scaffold may also create a slimmer top-level
>
> workers/
>
> directory that stages a subset of files for the background processor. The real, config-referenced plugin lives at
>
> `plugins/workers/`
>
> — that is what
>
> netscript.config.ts
>
> points at and where you author jobs.

## 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:

```ts
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 });
```

```ts
// 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.
```

> How tracing works here
>
> The framework wraps the whole execution in OpenTelemetry spans — dispatch, execution, duration, and status show up in the
>
> Aspire dashboard
>
> automatically. The
>
> createJobTools(ctx)
>
> helpers add handler detail to that active trace:
>
> trace.addEvent
>
> records events,
>
> trace.withChildSpan
>
> records timed child work, and
>
> progress(...)
>
> records progress while forwarding it to the workers runtime.
>
> log.*
>
> remains console-backed.

## 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 is a shorthand you install once
>
> ns-workers
>
> is a name
>
> you
>
> give the workers plugin's CLI — the scaffold does not create it. Install it once, globally, and every
>
> ns-workers
>
> command on this page works as written:
>
> ```bash
> deno install -gArf -n ns-workers jsr:@netscript/plugin-workers@0.0.6/cli
> ```
>
> Rather not install it? Each
>
> ns-workers <verb …>
>
> is exactly
>
> deno x -A jsr:@netscript/plugin-workers@0.0.6/cli <verb …>
>
> — run that full form instead.

```sh
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:

```ts
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.

> Triggering a job from your own code
>
> This chapter triggers the job through
>
> ns-workers
>
> (Step 5) so you can watch it run. From inside another NetScript runtime — a trigger or a scheduled job — you enqueue work with the builder's
>
> enqueueJob(...)
>
> step rather than an HTTP call; that path is covered in the
>
> webhook tutorial
>
> and the
>
> background-jobs capability
>
> . For this track, the runtime-backed CLI trigger is the clearest way to prove the job ran.

## 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.

> Restart the processor if it was already running
>
> If
>
> aspire start
>
> was up before you generated the registry, restart it (or let it hot-reload) so the Workers API and its background processor pick up the new job.

## 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](https://rickylabs.github.io/netscript/explanation/aspire/) 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:

```sh
# 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:

```sh
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](https://localhost:18888) — the framework emits the dispatch/execution span automatically.

**Workers API (endpoints used here)**

| 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 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.

> If the execution never appears
>
> - **Aspire isn't running** — the background processor that drains the queue is an Aspire resource. Start `aspire start` from `aspire/` and retry.
> - **The job isn't registered** — re-run `netscript generate plugins` so `provision-member` is in the registry, then restart Aspire.
> - **Wrong id** — the trigger path uses the job's `id` (`provision-member`), not its filename. Check `GET /api/v1/workers/jobs`.

## 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.

[3 · Workspace data](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/03-workspace-data/) [5 · Route authz](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/05-route-authz/)
