Add a queue and a cron schedule
Your import job runs one file at a time. A real ERP sync gets bursts — a supplier drops twenty files at once — and needs recurring work that no file triggers, like a nightly full re-sync. This chapter adds the two pieces that make that durable: a queue provider sized for throughput, and a cron schedule that fires on a cadence rather than an event.
What you will build
By the end of this chapter your workers config will name a queue provider and a worker
concurrency so bursts of imports drain in parallel instead of one-by-one, and a new
scheduled trigger (defineScheduledTrigger) will enqueue a re-sync job on a cron cadence. You
will also know the one concurrency naming gotcha to set explicitly so it does not bite you under
Aspire.
Before you begin
You need the my-erp/ workspace from Chapter 2 with the
import-products job and product-import-trigger working, and aspire start healthy. Confirm the job
is registered:
curl 'http://localhost:8091/api/v1/workers/jobs'
Expected: import-products appears in the list. If not, return to Chapter 2 and re-run
netscript generate plugins.
Step 1 — Choose a queue provider
A NetScript queue is provider-agnostic: the same job-enqueue path runs on the zero-config Deno KV
default on your laptop and on a real broker under Aspire, without touching your job code. The
QueueProvider enum has four selectable production backends, and auto-discovery picks one for you
when you do not pin one:
| Provider | How you select it | Reach for it when |
|---|---|---|
deno-kv |
Auto-discovery fallback, or pin QueueProvider.DenoKv |
Local dev and single-instance apps with no broker. Durable to the Deno KV store; needs --unstable-kv. |
redis |
Auto-discovered when a Redis/Garnet connection is present; or pin QueueProvider.Redis |
High-throughput production with a Redis/Garnet resource already provisioned by Aspire. |
rabbitmq |
Auto-discovered first when an AMQP broker is present; or pin QueueProvider.RabbitMQ |
Broker-grade routing and reliability; the top of the auto-discovery probe. |
postgres |
Explicit only — pass QueueProvider.Postgres; never auto-discovered |
You already run Postgres and want the queue in the same transactional store. Row-claim via FOR UPDATE SKIP LOCKED. |
For the ERP sync, the right default is to let the environment decide: write provider-neutral config
and you get Deno KV locally, and the Aspire cache once it is up — redis by default, or garnet via --cache-backend. The auto-discovery probe order is
RabbitMQ → Redis → Deno KV.
Step 2 — Size worker concurrency in config
concurrency on the workers config is the size of the worker pool the runner spins up — each slot is
its own V8 isolate (~20–40 MB), so raising it buys parallelism at a memory cost. This is where a
burst of imports gets drained in parallel. Set it in the generated worker config:
// config/official-plugins/mod.ts
import { defineWorkers } from '@netscript/plugin-workers-core/config';
export const workers = defineWorkers({
jobsDir: './workers/jobs',
tasksDir: './workers/tasks',
queueProvider: 'auto', // Deno KV locally; the Aspire cache once up (redis default, garnet alt).
queueName: 'jobs',
concurrency: 4, // pool size: 4 isolates → ~80–160 MB. Raise for throughput, lower to bound memory.
enabled: true,
groups: [],
});
| Name | Type | Description |
|---|---|---|
concurrency |
number |
Default worker pool size (V8 isolates running jobs in parallel). Schema default 2. |
queueProvider |
'auto' | 'deno-kv' | 'redis' | 'postgres' | 'amqp' |
Queue backend. 'auto' resolves one for you. Default 'auto'. |
queueName |
string |
Queue the runner consumes from. Default 'jobs'. |
jobsDir / tasksDir |
string |
Directories scanned for default-exported job and task modules. |
groups |
WorkerGroupData[] |
Per-topic worker groups, each with its own scaling.concurrency and retention. |
enabled |
boolean |
Whether workers run at all. Default true. |
For per-topic control — a hot imports topic at concurrency 10 while a heavy reports topic stays
at 1 — use a WorkerGroup with its own scaling: { mode, concurrency }. The full per-topic and
runner-mode knobs are in Tune the worker runtime.
Step 3 — Add a cron schedule
Some ERP work is time-driven, not file-driven: a nightly full re-sync, an hourly cleanup of stale
staging files. That is a scheduled trigger — defineScheduledTrigger(handler, spec) from
@netscript/plugin-triggers-core/builders. Like the file-watch trigger, its handler returns an array
of effects; here it enqueues a job on a cron cadence.
// plugins/triggers/scheduled-resync.ts
import { defineScheduledTrigger, enqueueJob } from '@netscript/plugin-triggers-core/builders';
import type { JobDefinition } from '@netscript/plugin-workers-core';
const importProductsJob = {
id: 'import-products' as JobDefinition<'import-products'>['id'],
name: 'Import Products',
topic: 'default',
entrypoint: './workers/jobs/import-products.ts',
} satisfies JobDefinition<'import-products'>;
export const dailyResyncSchedule = defineScheduledTrigger(
(event) => Promise.resolve([enqueueJob(importProductsJob, { payload: event.payload })]),
{
id: 'daily-resync-schedule',
cron: '0 6 * * *', // every day at 06:00
timezone: 'UTC',
description: 'Runs a full product re-sync every morning.',
tags: ['resync', 'products', 'scheduled'],
},
);
export default dailyResyncSchedule;
| Name | Type | Description |
|---|---|---|
id |
string |
Stable identifier for the schedule, used in logs and the events feed. |
cron |
string |
Standard 5-field cron expression. '0 6 * * *' = daily at 06:00; '*/5 * * * *' = every five minutes. |
timezone |
string |
IANA timezone the cron is evaluated in. Set it explicitly — 'UTC' here — so the cadence is unambiguous. |
description / tags |
string / string[] |
Metadata for discovery and the dashboard. |
Step 4 — Register the new trigger
The scheduled trigger has to be picked up by the triggers runtime. Regenerate the registries:
netscript generate plugins
Then restart aspire start (or let it hot-reload) so the triggers processor loads
daily-resync-schedule.
Verify your progress
First confirm the config type-checks with the new concurrency and provider settings:
deno task check
Expected: a clean check. Then confirm the cron schedule registered — for a fast feedback loop you can
temporarily set its cron to */2 * * * * (every two minutes), regenerate, restart Aspire, wait,
and read the executions feed:
curl 'http://localhost:8091/api/v1/workers/executions?limit=10'
Expected: an import-products execution appears on the cron cadence with no file dropped — the
schedule, not a file event, enqueued it. Set the cron back to 0 6 * * * when you are done testing.
- [ ]
config/official-plugins/mod.tssetsconcurrencyandqueueProvider. - [ ]
deno task checkis clean. - [ ]
daily-resync-scheduleis registered (afternetscript generate plugins+ Aspire restart). - [ ] A scheduled
import-productsexecution appears on the cron cadence with no file dropped. - [ ] You set
WORKERS_CONCURRENCYexplicitly (or rely on configconcurrency) rather than the ignored AspireWORKER_CONCURRENCY.
What you built
A workers config that names a queue provider and a worker concurrency so import bursts drain in
parallel, and a defineScheduledTrigger cron that enqueues a re-sync on a cadence — plus the
knowledge to set concurrency where it actually takes effect. The ERP sync now scales and runs
recurring work. The last chapter runs the whole thing under Aspire.