Pack-authoring quickstart
You are going to build a LumenFlow pack from zero. Manifest, one emitted event, one subscriber handler, one test. At the end, a conductor-mode sidecar will see your pack’s events on the wire.
Target time: 30 minutes. If you stall past that, something has drifted
from this page — file an issue or ping #packs with the failing step.
What you’ll build
Section titled “What you’ll build”A pack called acme-demo that:
- Declares a manifest with one emitted kind and one subscribed kind.
- Emits
acme-demo:job_completedwhen a mockrunJob()finishes. - Handles inbound
conductor:recovery_requestedfor its own jobs. - Passes a conformance test (emit + replay).
That’s the whole surface. Everything else — pack registry, runtime wiring, pack metadata validation — is already in the host.
Prerequisites
Section titled “Prerequisites”- LumenFlow workspace already set up (
pnpm lumenflow:integraterun at least once). - Node 20+. Repo’s
pnpm bootstrapgreen. - Scratch directory inside
packages/@lumenflow/packs/where your pack source will live.
Step 1 — Scaffold the pack directory (5 min)
Section titled “Step 1 — Scaffold the pack directory (5 min)”-
Create the directory:
-
Create
package.json: -
Create a minimal
tsconfig.json: -
pnpm installfrom the repo root to wire workspace dependencies.
Step 2 — Write the manifest (5 min)
Section titled “Step 2 — Write the manifest (5 min)”packages/@lumenflow/packs/acme-demo/src/manifest.ts:
Sanity check — run the manifest validator:
If typecheck passes, your manifest is structurally valid. The runtime will assert consistency (every emitted kind must match a namespacing rule, every subscribed kind must have a registered handler) at pack load later.
Step 3 — Write the emitter (5 min)
Section titled “Step 3 — Write the emitter (5 min)”packages/@lumenflow/packs/acme-demo/src/emit.ts:
The builder populated schema_version=2, timestamp, and event_id
automatically. The idempotencyKey makes event_id deterministic —
retries produce the same id, cloud dedupes cleanly.
Step 4 — Write the subscriber handler (5 min)
Section titled “Step 4 — Write the subscriber handler (5 min)”packages/@lumenflow/packs/acme-demo/src/handlers/recovery-requested.ts:
Note: the handler dedupes by event_id, uses an upsert-shaped side
effect (markJobRecovered is idempotent), and records the replay key
after the effect. ADR-013 §2 at-least-once tolerance is met.
Step 5 — Barrel + register (3 min)
Section titled “Step 5 — Barrel + register (3 min)”packages/@lumenflow/packs/acme-demo/src/index.ts:
Register the pack with the host (the host’s pack-loading config is
project-local — the canonical example is
packages/@lumenflow/host/src/pack-loader.ts):
Step 6 — Write the test (5 min)
Section titled “Step 6 — Write the test (5 min)”packages/@lumenflow/packs/acme-demo/__tests__/emit-replay.test.ts:
Run it:
Two green tests. First proves the envelope is built correctly; second proves the handler is idempotent. Both are the minimum bar the conformance test will check.
Step 7 — See it on the wire (2 min)
Section titled “Step 7 — See it on the wire (2 min)”From the repo root, with a conductor-mode enrollment token installed
(.lumenflow/state/conductor/enrollment.json):
The cloud subscriber (or your local mem:inbox if you are running the
loopback adapter) sees an acme-demo:job_completed event with a stable
event_id. You are done.
What you skipped — and when to add it
Section titled “What you skipped — and when to add it”This quickstart deliberately kept scope minimal. Ship the pack first, then layer on:
| Feature | When to add |
|---|---|
required_approvals | When your pack exposes a destructive surface (phone send, wu destroy). |
queue backpressure for approvals | When you add an approval_requested kind. |
surfaces_required: ['sidekick-channel'] | When your pack integrates with the sidekick bridge. |
@idempotent decoration | When the conformance test (INIT-060 WU-11) lands repo-wide. |
| Cross-pack event subscription | When your pack reacts to another pack’s events (e.g. software-delivery:gate_failed). |
Every one of these is an additive change. None of them invalidate the manifest or emitter you just wrote.
Troubleshooting
Section titled “Troubleshooting”- “Unknown kind at emit” — the kind you passed to
buildKernelEventV2is not in your manifest’semitted_event_kinds. The runtime rejects unknown kinds to protect the cloud event-schema registry. - “Pack slug prefix mismatch” — your kind starts with a prefix that
is not your manifest’s
slug. Rule:<slug>:<event_name_snake_case>. - “Missing backpressure_policy entry” — you declared an emitted kind
but did not pick
ephemeralvsqueuefor it. The validator refuses to load a pack with an under-declared policy. - “Handler called twice” — the conformance harness is testing
idempotency. Your handler is missing its
replay_log.has()check or its effect is non-upsert-shaped.