Skip to content

Monorepo worker pools (discover: for N near-identical services)

When to use this: the stack has 3+ owned services with the same shape — typically a directory of *Worker.ts / *Processor.ts / *Handler.ts files, each spawned as its own process with the same ready_when / fail_when / env. Hand-writing N owned entries that mostly repeat is the obvious first attempt; the problem is it scales with N (an 11-worker stack → 110+ lines of yaml that all look the same).

The naive workaround — one owned entry wrapping concurrently — loses per-worker logs / restart state / health. The fix is a discover: block: a single owned entry expands at parse time into N synthetic owned services, each with its own state slot.

yaml
owned:
  workers:
    discover:
      # Glob is relative to discover.cwd (or parent.cwd, or the config dir).
      glob: "src/temporal/workers/*TemporalWorker.ts"
      name_template: "${basename_no_ext | strip_suffix:TemporalWorker | kebab}-worker"
      cmd_template: "pnpm exec nodemon -r ./tsconfigPathsDist.js dist/temporal/workers/${basename_no_ext}.js"
      cwd: apps/workers
    # Every field below applies to EVERY discovered instance — write once.
    ready_when:
      log_match: "Temporal worker created successfully|state: 'RUNNING'"
    fail_when:
      log_match: "FATAL|UnhandledPromiseRejection"
    env:
      NODE_ENV: development

For apps/workers/src/temporal/workers/{Email,Payment,Cleanup}TemporalWorker.ts, this materializes into three synthetic owned services: email-worker, payment-worker, cleanup-worker. Each has its own log file, its own restart state, its own dashboard tile — identical to a hand-written owned service.

Adding BillingTemporalWorker.ts to the workers dir adds billing-worker to the stack on the next lich up — no yaml edit.

Mutual exclusivity: an entry with discover: MUST NOT also set cmd: at the entry root — the per-instance command lives on discover.cmd_template. lich validate rejects the combination.

Template grammar: see the Glob-based discovery section in lich-yaml-spec.md for the full vars + filters reference. The short version: ${basename}, ${basename_no_ext}, ${dirname}, with | kebab | snake | strip_suffix:X | strip_prefix:X filters chainable left to right.

Common mistake: reaching for discover: for two services. The indirection costs more than it saves; write them out. The break-even is around three near-identical services.

Other common mistake: writing per-worker ready_when / fail_when patterns that are subtly different (one worker watches "Worker started", another "started OK"). discover: applies the parent's shared fields verbatim — if patterns diverge, the workers don't fit a discover block. Either unify the patterns or fall back to per-worker hand-written entries.

Released under the MIT License.