# Run it locally under Aspire

You have built the whole SAP→Dynamics sync: a file-watch import job, a sandboxed transform task, a queue, and a cron schedule. This final chapter runs all of it together — workers, triggers, queue, and cron processors — on one machine under `aspire start`, and shows you how to read the running system from the dashboard. It is the **local** orchestration story: one command, one observable stack, throwaway infrastructure.

And you wrote none of the wiring that makes that possible. `netscript init` scaffolded the AppHost back in Chapter 1; every plugin you added since contributed its own API and background processor to the graph on its own; a single `aspire start` then boots the lot with cross-references resolved for you. That is the scaffold-to-Aspire story this chapter surfaces end to end — from an empty folder to a live, correctly-wired, observable stack with no hand-written orchestration config in between.

1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/)
2. [2 · Import job](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/02-import-job/)
3. [3 · Polyglot transform](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/03-polyglot-transform/)
4. [4 · Queue & cron](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/04-queue-and-cron/)
5. [5 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/05-deploy/)

## What you will build

By the end of this chapter you will bring the full `my-erp/` resource graph up with a single `aspire start` — Postgres, Redis, the workers API + processor, and the triggers API + processor (which runs your file-watch and cron triggers) — initialize the database through the running AppHost, and read the live import pipeline in the Aspire dashboard: resources, console logs, and the traces that stitch a file drop to its job execution.

> Aspire is the LOCAL story — not a production deployer
>
> aspire start
>
> exists to make
>
> git clone
>
> → one command produce a complete, observable, correctly-wired stack on
>
> one machine
>
> . The Postgres and Redis it starts are throwaway Docker containers for dev convenience —
>
> not
>
> your production database or cache. Shipping to a remote target (managed infrastructure, your own process lifecycle) is the
>
> Deploy
>
> recipe; this chapter is the local companion. The full local walkthrough lives in
>
> Deploy locally with Aspire
>
> .

## Before you begin

You need the complete `my-erp/` workspace from [Chapter 4](https://rickylabs.github.io/netscript/tutorials/erp-sync/04-queue-and-cron/): the workers and triggers plugins, the `import-products` job, the `normalize-sap` transform task, the `product-import-trigger` file watch, and the `daily-resync-schedule` cron. Docker must be running so Aspire can provision Postgres and Redis. Confirm the AppHost was scaffolded (it is a TypeScript/Node program, not C#):

```sh
ls aspire/apphost.mts aspire/aspire.config.json
```

Expected: both files exist. `netscript init` generated them in Chapter 1; you never hand-write them.

## Step 1 — Understand the graph you are about to run

The resource graph is **derived from your installed plugins** at boot via `composeAppHost` — add a plugin and its API plus background processor appear; remove it and they vanish, no edit to `apphost.mts`. With workers and triggers installed, a single `aspire start` stands up this graph:

**What aspire start brings up for the ERP sync**

| Name | Type | Description |
| --- | --- | --- |
| `aspire (dashboard)` | `https://localhost:18888 / http://localhost:18889` | Live resource list, console logs, structured logs and traces. A login token is printed on start. |
| `OTLP collector` | `http://localhost:4318` | OpenTelemetry endpoint the dashboard runs; framework spans and structured logs land here automatically. |
| `postgres` | `Container` | Throwaway Docker Postgres. The database netscript db commands target — reachable only while Aspire is up. |
| `redis` | `Container (cache)` | Redis cache — the default `--cache-backend`; Redis-compatible. Backs the KV/queue workloads — this is what auto-discovery resolves the queue to once it is up. |
| `workers API + processor` | `allocated port + executable` | The API enqueues; a separate background processor drains the queue and runs your import job. |
| `triggers API + processor` | `allocated port + executable` | The API plus the processor that runs your file-watch and cron triggers. |

> Plugin API ports are allocated per project, not fixed
>
> A plugin runtime installed without
>
> --port
>
> gets a host port the installer picks: a hash of your
>
> project name
>
> over the IANA dynamic range (
>
> 49152–65535
>
> ), probing upward past ports already claimed in this workspace. That keeps two of your own projects apart in practice, but it is not a guarantee — the range is finite and workspaces cannot see each other's allocations — and no number printed here would match yours. The dashboard's
>
> Resources
>
> tab is the authority for the exact port each resource bound. Read it from there.

## Step 2 — Restore and run

The AppHost runs on its own isolated Node runtime inside `aspire/` so its dependencies never leak into your Deno workspace. Restore once per machine, then run — both from inside `aspire/`:

```sh
cd aspire
aspire restore   # one-time SDK restore (and after an SDK bump)
aspire start       # boots the whole graph; prints the dashboard URL + a login token
```

`aspire start` brings up infrastructure first, then the plugin APIs and background processors, with cross-references resolved into injected environment variables. Leave it running.

## Step 3 — Initialize the database through the running AppHost

With Aspire up, Postgres is live and the `netscript db` commands can reach it. Run them from the **workspace root** in a second terminal (leave `aspire start` going in the first):

```sh
netscript db init --name init   # create + apply the first migration
netscript db generate           # generate the Prisma client
netscript db seed               # optional: seed development data
```

These talk to the Postgres container Aspire provisioned. Run them with no Aspire up and they fail — there is no Postgres for them to reach.

> Order matters: scaffold → orchestrate → database
>
> The single most common first-run error is running a
>
> netscript db
>
> command before
>
> aspire start
>
> .
>
> aspire restore
>
> and
>
> aspire start
>
> run from inside
>
> aspire/
>
> ;
>
> netscript db
>
> commands run from the
>
> workspace root
>
> ,
>
> after
>
> the graph is up. Mixing the directories or the order is what breaks. There is no
>
> netscript generate aspire
>
> — the AppHost is produced by
>
> netscript init
>
> .

## Step 4 — Watch the pipeline run end to end

With the full stack up, exercise the pipeline you built and read it from the dashboard. Drop a file in the SAP export shape:

```sh
cat > .data/incoming/products_live.csv <<'CSV'
material_no,description,price_cents
ANV-9,Anvil,4999
CSV
mv .data/incoming/products_live.csv .data/incoming/products/products_live.csv
```

Then open `https://localhost:18888`, paste the login token `aspire start` printed, and use the three dashboard surfaces:

**Reading the running ERP sync in the Aspire dashboard**

| Name | Type | Description |
| --- | --- | --- |
| `Resources` | `tab` | Every container and executable with status and the port it bound. Confirm workers, triggers, postgres, and redis are all green. |
| `Console logs` | `tab` | stdout/stderr per resource. Open the workers processor to read your import job's log lines; the triggers processor shows the file-watch event. |
| `Structured logs + Traces` | `tab` | Spans correlated by traceparent. The framework instruments job dispatch and execution automatically, so a file drop → job run trace appears with no extra wiring. |

Because Aspire starts each resource with an OTLP endpoint pointed at `http://localhost:4318`, framework-level spans (job dispatch, job execution, scheduler runs) surface in the **Traces** view on their own.

> Which spans you see
>
> The
>
> dispatch/execution trace appears automatically
>
> as the framework instruments the run end to end. Handler calls through
>
> createJobTools
>
> , including
>
> trace.withChildSpan
>
> , add custom detail beneath that trace.
>
> log.*
>
> remains console-backed. See
>
> Observability
>
> for the complete path.

## Verify your progress

Confirm the whole graph is healthy and the pipeline ran:

```sh
# Both plugin APIs answer (endpoints from the Resources tab).
curl <workers-endpoint>/health
curl <triggers-endpoint>/health

# The dropped file produced an import execution.
ns-workers executions --limit=10 --json
```

> 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 the
>
> ns-workers
>
> command above works as written:
>
> ```bash
> deno install -gArf -n ns-workers jsr:@netscript/plugin-workers@0.0.6/cli
> ```
>
> Rather not install it?
>
> ns-workers <verb …>
>
> is exactly
>
> deno x -A jsr:@netscript/plugin-workers@0.0.6/cli <verb …>
>
> — run that full form instead.

Expected: both health checks return healthy JSON, and the executions feed shows a completed `import-products` run for `products_live.csv`.

- [ ] `aspire start` is up; the dashboard lists `postgres`, `redis`, workers, and triggers all green.
- [ ] `netscript db init/generate` succeeded against the Aspire Postgres.
- [ ] `curl <workers-endpoint>/health` and `curl <triggers-endpoint>/health` both return healthy.
- [ ] A file drop produced an `import-products` execution and a dispatch/execution trace in the dashboard.

> Footguns when aspire start will not boot
>
> - **Docker not running** — Aspire provisions Postgres + Redis through Docker; no daemon means the happy path does not start.
> - **Wrong directory** — `aspire restore`/`aspire start` run from `aspire/`; `netscript db` runs from the workspace root.
> - **Ports in use** — the dashboard wants `:18888`/`:18889` and OTLP `:4318`; a stale prior run holding a port blocks boot. Free it and retry.

## What you built

You ran the complete SAP→Dynamics sync — workers, triggers, queue, and cron — on one machine under a single `aspire start`, initialized the database through the running AppHost, and watched a SAP export flow into a durable job execution with its trace in the dashboard. You now have an end-to-end durable background-processing backend that could carry a real migration's parallel-run, and you know exactly where the local story ends and a production deployment begins.

## Where to go next

You have finished the ERP Sync track. From here, branch into task-oriented and reference docs:

- **Ship it remotely** → [Deploy](https://rickylabs.github.io/netscript/orchestration-runtime/how-to/deploy/) — the production companion to this local run: deployable units, managed backing services, and the `--no-aspire` path.
- **Take the transform polyglot** → [Run a polyglot task](https://rickylabs.github.io/netscript/background-processing/how-to/run-a-polyglot-task/) — swap Chapter 3's Deno transform for a Python or shell step on your own host.
- **Tune throughput** → [Choose a queue provider](https://rickylabs.github.io/netscript/data-persistence/how-to/choose-a-queue-provider/) and [Tune the worker runtime](https://rickylabs.github.io/netscript/background-processing/how-to/tune-worker-runtime/).
- **Understand the orchestrator** → [Orchestration with Aspire](https://rickylabs.github.io/netscript/explanation/aspire/) and the full [How-to guides](https://rickylabs.github.io/netscript/how-to/).

[4 · Queue & cron](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/04-queue-and-cron/) [How-to guides](https://rickylabs.github.io/netscript/netscript/how-to/)
