@n8n/scheduler 1.7.1 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -31
- package/dist/build.tsbuildinfo +1 -1
- package/dist/core/errors.d.ts +31 -0
- package/dist/core/errors.js +60 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/factory.d.ts +6 -0
- package/dist/core/factory.js +89 -0
- package/dist/core/factory.js.map +1 -1
- package/dist/core/index.d.ts +7 -3
- package/dist/core/index.js +15 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/lifecycle/options.d.ts +2 -0
- package/dist/core/lifecycle/options.js +2 -0
- package/dist/core/lifecycle/options.js.map +1 -1
- package/dist/core/materializer/coalesce-group.js +1 -3
- package/dist/core/materializer/coalesce-group.js.map +1 -1
- package/dist/core/materializer/index.d.ts +1 -0
- package/dist/core/materializer/index.js +4 -1
- package/dist/core/materializer/index.js.map +1 -1
- package/dist/core/materializer/owner-key.d.ts +17 -0
- package/dist/core/materializer/owner-key.js +17 -0
- package/dist/core/materializer/owner-key.js.map +1 -0
- package/dist/core/provisioning/index.d.ts +1 -1
- package/dist/core/provisioning/provisioner.d.ts +9 -3
- package/dist/core/provisioning/provisioner.js +15 -0
- package/dist/core/provisioning/provisioner.js.map +1 -1
- package/dist/core/reconciliation/index.d.ts +4 -0
- package/dist/core/reconciliation/index.js +10 -0
- package/dist/core/reconciliation/index.js.map +1 -0
- package/dist/core/reconciliation/options.d.ts +8 -0
- package/dist/core/reconciliation/options.js +12 -0
- package/dist/core/reconciliation/options.js.map +1 -0
- package/dist/core/reconciliation/owner.d.ts +10 -0
- package/dist/core/reconciliation/owner.js +34 -0
- package/dist/core/reconciliation/owner.js.map +1 -0
- package/dist/core/reconciliation/reconcile.d.ts +38 -0
- package/dist/core/reconciliation/reconcile.js +228 -0
- package/dist/core/reconciliation/reconcile.js.map +1 -0
- package/dist/core/reconciliation/store.d.ts +13 -0
- package/dist/core/reconciliation/store.js +3 -0
- package/dist/core/reconciliation/store.js.map +1 -0
- package/dist/core/recurrence/field.d.ts +2 -2
- package/dist/core/recurrence/field.js.map +1 -1
- package/dist/core/recurrence/kinds/cron.d.ts +2 -2
- package/dist/core/recurrence/kinds/cron.js.map +1 -1
- package/dist/core/recurrence/kinds/interval.d.ts +2 -2
- package/dist/core/recurrence/kinds/interval.js.map +1 -1
- package/dist/core/recurrence/kinds/one-off.d.ts +2 -2
- package/dist/core/recurrence/kinds/one-off.js.map +1 -1
- package/dist/core/recurrence/kinds/recurring-cron.d.ts +2 -2
- package/dist/core/recurrence/kinds/recurring-cron.js.map +1 -1
- package/dist/core/recurrence/resolve.d.ts +4 -2
- package/dist/core/recurrence/resolve.js +15 -0
- package/dist/core/recurrence/resolve.js.map +1 -1
- package/dist/core/scheduler.d.ts +2 -0
- package/dist/core/types/schedule.d.ts +2 -1
- package/dist/observability/attributes.d.ts +5 -0
- package/dist/observability/attributes.js +5 -0
- package/dist/observability/attributes.js.map +1 -1
- package/dist/observability/metrics.d.ts +1 -0
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/metrics.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -14,13 +14,13 @@ restarts, the timers are gone. If you run several copies of the server for high
|
|
|
14
14
|
availability, every copy fires the same timer and the work runs many times.
|
|
15
15
|
|
|
16
16
|
This package takes a different approach: **write the work down before running it.**
|
|
17
|
-
Every upcoming run is recorded in
|
|
18
|
-
there. Because the source of truth lives in storage and not in one process's memory:
|
|
17
|
+
Every upcoming run is recorded in shared, durable storage first, then picked up and
|
|
18
|
+
fired from there. Because the source of truth lives in storage and not in one process's memory:
|
|
19
19
|
|
|
20
20
|
- a restart or crash loses nothing (the run is still recorded and gets picked up
|
|
21
21
|
again),
|
|
22
22
|
- any server in the cluster can pick up any run, and
|
|
23
|
-
- coordination
|
|
23
|
+
- coordination through the shared store drives each run toward firing on **exactly one**
|
|
24
24
|
server, with safety mechanisms (claims, leases, recovery) to fall back on when a
|
|
25
25
|
server crashes mid-run.
|
|
26
26
|
|
|
@@ -44,10 +44,9 @@ From then on it:
|
|
|
44
44
|
- recovers runs that were stranded by a crashed server,
|
|
45
45
|
- cleans up finished runs after a retention window.
|
|
46
46
|
|
|
47
|
-
**How it is built.** The package is **pure logic**. It
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
scheduler composes its algorithms over them. This keeps every algorithm easy to
|
|
47
|
+
**How it is built.** The package is **pure logic**. It owns no storage and uses no
|
|
48
|
+
dependency injection directly. Instead the host hands it a small set of storage
|
|
49
|
+
functions and configuration, and the scheduler composes its algorithms over them. This keeps every algorithm easy to
|
|
51
50
|
test in isolation with a fake store. The work is split into a few focused
|
|
52
51
|
submodules, described below.
|
|
53
52
|
|
|
@@ -58,8 +57,10 @@ path during rollout.
|
|
|
58
57
|
## The moving parts
|
|
59
58
|
|
|
60
59
|
The scheduler runs four small background routines, each on its own repeating
|
|
61
|
-
timer
|
|
62
|
-
|
|
60
|
+
timer, plus an optional fifth when owner reconciliation is configured (see
|
|
61
|
+
[The reconciliation sweep](#the-reconciliation-sweep)). Each one is a separate
|
|
62
|
+
submodule with a single responsibility. Their code names are in parentheses; the
|
|
63
|
+
rest of this document uses the plain names.
|
|
63
64
|
|
|
64
65
|
```mermaid
|
|
65
66
|
flowchart TD
|
|
@@ -76,6 +77,8 @@ flowchart TD
|
|
|
76
77
|
T -.->|"finished runs,<br/>past their window"| C["Cleanup<br/><i>(retention)</i>"]
|
|
77
78
|
C -.->|deletes| T
|
|
78
79
|
|
|
80
|
+
RC["Reconciliation<br/><i>(owner sweep, optional)</i>"] -.->|"quarantines rules<br/>whose owner is gone"| J
|
|
81
|
+
|
|
79
82
|
classDef store fill:#2d3748,stroke:#4a5568,color:#fff;
|
|
80
83
|
class J,T store;
|
|
81
84
|
```
|
|
@@ -125,9 +128,9 @@ Successful and failed runs can be kept for different lengths of time.
|
|
|
125
128
|
is the next run? It handles cron expressions, intervals, one-offs, and the
|
|
126
129
|
"every N periods" variant described below, including timezone and daylight-saving
|
|
127
130
|
behaviour.
|
|
128
|
-
- **Lifecycle** is the set of repeating loops that drive the four passes above
|
|
129
|
-
each on its own interval with a little randomness
|
|
130
|
-
don't all fire their passes in lockstep.
|
|
131
|
+
- **Lifecycle** is the set of repeating loops that drive the four passes above
|
|
132
|
+
(five with reconciliation), each on its own interval with a little randomness
|
|
133
|
+
("jitter") so multiple servers don't all fire their passes in lockstep.
|
|
131
134
|
|
|
132
135
|
## Schedule kinds
|
|
133
136
|
|
|
@@ -216,18 +219,152 @@ Schedule and poll triggers both use `skip`: it matches what n8n did before the
|
|
|
216
219
|
durable scheduler, where a missed run was never replayed. Running late is opt-in
|
|
217
220
|
per job, which is where `coalesce` and `coalesce_owner` come in.
|
|
218
221
|
|
|
222
|
+
## Owning scheduled jobs
|
|
223
|
+
|
|
224
|
+
Every scheduled job belongs to something: a published workflow, an instance-level
|
|
225
|
+
maintenance task, an agent. That something is its **owner**, and it is plain data on
|
|
226
|
+
the job row — three fields, none of which this package interprets:
|
|
227
|
+
|
|
228
|
+
| Field | Meaning |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `ownerType` | what kind of thing owns the job (`workflow`, `system-task`, `agent`, …) |
|
|
231
|
+
| `ownerId` | which one (a workflow id, a task name, an agent id) |
|
|
232
|
+
| `ownerMemberId` | optionally, which part of it (the trigger node for a workflow, the scheduled task for an agent) |
|
|
233
|
+
|
|
234
|
+
Because ownership is data rather than a schema relationship, any part of the product
|
|
235
|
+
can own scheduled jobs without the scheduler learning what its owners are, and
|
|
236
|
+
without a migration. A job with nothing else to own it is **self-owned**: its
|
|
237
|
+
`ownerId` is its own name. There is no such thing as an ownerless job, so there is
|
|
238
|
+
no second code path for one.
|
|
239
|
+
|
|
240
|
+
### The two obligations of a new owner type
|
|
241
|
+
|
|
242
|
+
Ownership is a contract, not just a label. A module that owns scheduled jobs takes on
|
|
243
|
+
**both** of the following. Neither substitutes for the other, and shipping an owner
|
|
244
|
+
type without them leaves schedules running for things that no longer exist.
|
|
245
|
+
|
|
246
|
+
**1. Deprovision inside your own delete transaction.**
|
|
247
|
+
|
|
248
|
+
There is no storage-level cascade behind these rows. When your owner goes away,
|
|
249
|
+
*you* delete its jobs, in the same transaction as the write that removes the owner:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// Inside the transaction that stops the owner from existing.
|
|
253
|
+
await myOwnerRepository.remove(trx, ownerId);
|
|
254
|
+
await jobProvisioner.deprovisionOwnerInTransaction(trx, { ownerType: 'my-thing', ownerId });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Committing the two together is what makes the guarantee: a crash can never leave a job
|
|
258
|
+
firing for an owner that is already gone. Deprovisioning is idempotent, so a retried
|
|
259
|
+
teardown is safe, and a path that no longer knows which of an owner's parts
|
|
260
|
+
provisioned what can delete the whole owner at once.
|
|
261
|
+
|
|
262
|
+
Where the teardown genuinely cannot share one transaction, **deprovision first** and
|
|
263
|
+
remove the owner second. A crash between the two then leaves an owner with no
|
|
264
|
+
schedules, which the next attempt fixes, rather than schedules with no owner, which
|
|
265
|
+
only the sweep below would ever find.
|
|
266
|
+
|
|
267
|
+
n8n's own `workflow` owner type is the worked example of both shapes: a deactivation
|
|
268
|
+
deprovisions inside the transaction that writes `active = false` (`WorkflowService`),
|
|
269
|
+
while an unpublish deprovisions the workflow's jobs immediately before removing the
|
|
270
|
+
`workflow_published_version` mapping that made it an owner
|
|
271
|
+
(`WorkflowPublicationApplier`).
|
|
272
|
+
|
|
273
|
+
**2. Register a liveness resolver.**
|
|
274
|
+
|
|
275
|
+
The scheduler cannot tell whether one of your owners still exists, so you answer that
|
|
276
|
+
question for it: register a resolver for your owner type that, given owner ids, says
|
|
277
|
+
which of them are still there.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
registry.register('my-thing', {
|
|
281
|
+
async findExisting(ownerIds) {
|
|
282
|
+
return await myOwners.findExistingIds(ownerIds); // a Set of the ids that remain
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Build the registry in one place, a single manifest the host owns: declare every
|
|
288
|
+
owner module there, and let both readers (the provisioner and the scheduler's
|
|
289
|
+
reconciliation pass) build theirs from it, so neither can see an owner the other
|
|
290
|
+
does not.
|
|
291
|
+
|
|
292
|
+
Two rules make the answer safe to act on:
|
|
293
|
+
|
|
294
|
+
- **Absence means gone.** An id you leave out of the result is read as a positive
|
|
295
|
+
statement that the owner no longer exists. Never omit an id your lookup simply did
|
|
296
|
+
not cover.
|
|
297
|
+
- **Throw when you cannot tell.** If the lookup fails (a data source is unreachable, a
|
|
298
|
+
dependency is not ready), throw. "Could not tell" is never treated as "gone", and
|
|
299
|
+
the sweep leaves every job of your owner type exactly as it was.
|
|
300
|
+
|
|
301
|
+
Provisioning **refuses** an owner type with no registered resolver, so a new kind of
|
|
302
|
+
owner cannot ship without a cleanup story. The refusal is built into
|
|
303
|
+
`createJobProvisioner` itself: the registry is one of its required dependencies.
|
|
304
|
+
|
|
305
|
+
### The reconciliation sweep
|
|
306
|
+
|
|
307
|
+
The resolvers feed a periodic sweep, the safety net behind obligation 1. It exists for
|
|
308
|
+
what synchronous deprovisioning cannot cover: a process killed between two writes, a
|
|
309
|
+
teardown path written before it knew about scheduled jobs, an owner deleted straight
|
|
310
|
+
from storage.
|
|
311
|
+
|
|
312
|
+
It runs as a fifth lifecycle loop, composed only when the host passes `reconciliation`
|
|
313
|
+
to `createScheduler`, so a deployment without it pays nothing. It follows the same
|
|
314
|
+
jittered cadence as the other passes, every fifteen minutes by default with a five
|
|
315
|
+
minute timeout, and pages through live jobs in bounded batches so one pass never scans
|
|
316
|
+
the whole table.
|
|
317
|
+
|
|
318
|
+
Because it acts on an answer from code it does not own, it never deletes on first
|
|
319
|
+
sight:
|
|
320
|
+
|
|
321
|
+
```mermaid
|
|
322
|
+
stateDiagram-v2
|
|
323
|
+
[*] --> Running: rule provisioned
|
|
324
|
+
Running --> Quarantined: owner reported gone<br/>(clock stopped, queued runs withdrawn)
|
|
325
|
+
Quarantined --> Running: owner reported alive again<br/>(clock recomputed)
|
|
326
|
+
Quarantined --> [*]: still gone past the grace period<br/>(deleted)
|
|
327
|
+
Running --> Running: owner reported alive
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- A job whose owner is reported gone has its **clock stopped and is stamped**, and its
|
|
331
|
+
already-queued runs are withdrawn, so it stops firing immediately. Its `enabled` flag is
|
|
332
|
+
left as it was: the sweep never turns a job on or off.
|
|
333
|
+
- It is **deleted only once that stamp is older than the quarantine grace** (a day by
|
|
334
|
+
default), and only while its owner is still reported gone.
|
|
335
|
+
- A stamped job whose owner turns out to exist after all is **revived** with a freshly
|
|
336
|
+
computed clock, so a resolver bug corrected inside the grace window destroys nothing. A
|
|
337
|
+
job that was disabled before the quarantine comes back disabled, with no clock.
|
|
338
|
+
|
|
339
|
+
A resolver answers whether an owner exists, not what it currently wants, so a revival
|
|
340
|
+
restores the job exactly as it was stored. A job the owner would no longer provision
|
|
341
|
+
(a part of it dropped while its jobs were quarantined) comes back too, and stays
|
|
342
|
+
runnable until the owner provisions or deprovisions again and removes it. Provisioning
|
|
343
|
+
is what reconciles an owner's set of jobs; the sweep only decides whether they may run
|
|
344
|
+
at all.
|
|
345
|
+
|
|
346
|
+
Two more guardrails: an owner type whose resolver throws, or that nothing claimed, is
|
|
347
|
+
left entirely alone; and jobs younger than a short settle period are skipped, so an
|
|
348
|
+
owner written just after its jobs is never mistaken for one that never existed.
|
|
349
|
+
|
|
350
|
+
The third layer, behind both of these, is the handler itself: a task whose subject has
|
|
351
|
+
disappeared should no-op and report "not dispatched" rather than fail. n8n's trigger
|
|
352
|
+
handlers do that for a node they can no longer resolve and for an occurrence already
|
|
353
|
+
executed, but not yet for a workflow whose published version is gone: that one fails the
|
|
354
|
+
occurrence, so it retries and dead-letters until the sweep retires the job.
|
|
355
|
+
|
|
219
356
|
## Durable and distributed
|
|
220
357
|
|
|
221
358
|
Two properties describe the whole design, and everything else follows from them.
|
|
222
359
|
|
|
223
360
|
**Durable** means nothing important lives only in memory. The rules, every upcoming
|
|
224
|
-
run, and each run's current state all live in the
|
|
361
|
+
run, and each run's current state all live in the shared store. A process can restart
|
|
225
362
|
or crash at any moment and the scheduler simply picks up where the stored state left
|
|
226
363
|
off. Compare an in-memory scheduler, where a restart forgets every pending timer.
|
|
227
364
|
|
|
228
365
|
**Distributed and leaderless** means there is no elected "scheduler node" that the
|
|
229
366
|
others depend on. Every server runs the same passes over the same shared state, and
|
|
230
|
-
they coordinate purely through the
|
|
367
|
+
they coordinate purely through the shared store. There is no leader to elect, fail over,
|
|
231
368
|
or lose. Adding or removing a server changes throughput, not correctness: any server
|
|
232
369
|
can plan any rule and run any due task.
|
|
233
370
|
|
|
@@ -240,9 +377,9 @@ over never repeating it.** In rare recovery situations that can mean a run execu
|
|
|
240
377
|
more than once, so handlers should tolerate being called again for the same run.
|
|
241
378
|
The design leans on a few ideas working together.
|
|
242
379
|
|
|
243
|
-
- **Claim.** A run is only ever picked up through an atomic
|
|
244
|
-
single statement that flips exactly one waiting row to "running" and stamps it
|
|
245
|
-
with the server that claimed it. Because the
|
|
380
|
+
- **Claim.** A run is only ever picked up through an atomic claim in the shared
|
|
381
|
+
store: a single statement that flips exactly one waiting row to "running" and stamps it
|
|
382
|
+
with the server that claimed it. Because the store resolves the race, only one
|
|
246
383
|
server can win a given run, no matter how many try at once.
|
|
247
384
|
- **Lease.** A claim comes with an expiry (a *lease*). While the lease is valid the
|
|
248
385
|
run belongs to that server. If the server dies, the lease lapses and the recovery
|
|
@@ -328,8 +465,8 @@ stateDiagram-v2
|
|
|
328
465
|
## What this package is (and is not)
|
|
329
466
|
|
|
330
467
|
It is a **library of scheduling logic**, not a running service on its own. It owns
|
|
331
|
-
the algorithms and the schedule math; it does not own a
|
|
332
|
-
|
|
468
|
+
the algorithms and the schedule math; it does not own storage, a DI container, or
|
|
469
|
+
a configuration source. A **host** provides those and drives it.
|
|
333
470
|
|
|
334
471
|
The public surface is small:
|
|
335
472
|
|
|
@@ -345,7 +482,7 @@ The public surface is small:
|
|
|
345
482
|
- A few pure helpers are exported too: schedule validation, next-run computation,
|
|
346
483
|
and the provisioning entry points.
|
|
347
484
|
|
|
348
|
-
In n8n, that host is the
|
|
485
|
+
In n8n, that host is the server's durable-scheduler module, which wires up
|
|
349
486
|
DI, configuration and instance identity, and routes the scheduler's events to the
|
|
350
487
|
logger. While `N8N_SCHEDULER_ENABLED` is off, the previous in-memory engine stays
|
|
351
488
|
in place; turning the flag off again is the rollback.
|
|
@@ -354,7 +491,7 @@ The boundary this section describes is enforced, not just documented.
|
|
|
354
491
|
`src/__tests__/dependency-purity.test.ts` fails if the package ever gains a
|
|
355
492
|
forbidden dependency, whether a new `package.json` entry outside the allowlist or
|
|
356
493
|
an `import` of something effectful (`@n8n/db`, TypeORM, `n8n-core`, the DI
|
|
357
|
-
container, the
|
|
494
|
+
container, the host server package, or a Node I/O built-in). Its opening doc comment is
|
|
358
495
|
the living explanation of the approach: why scheduling is split from execution and
|
|
359
496
|
storage, why each excluded dependency is excluded, and how far the idea could be
|
|
360
497
|
taken. Read it first if you want the reasoning behind the split above.
|
|
@@ -394,7 +531,7 @@ A few things that are not obvious from the code but save a lot of confusion.
|
|
|
394
531
|
event sink at `warn` level, so they land in the host's logs:
|
|
395
532
|
- a **clock-skew warning** at start-up. Due-ness and leases are judged on the
|
|
396
533
|
clock the scheduler coordinates on (the host supplies it via `now`, e.g. the
|
|
397
|
-
|
|
534
|
+
shared store's clock), but fire timers are armed on this instance's own clock. When
|
|
398
535
|
`start` samples the two and they differ by more than the threshold (default
|
|
399
536
|
`1s`, `CLOCK_SKEW_WARN_THRESHOLD_MS`), it warns: tasks may fire early or late,
|
|
400
537
|
and the instance clock should be synchronised (e.g. via NTP). The sample runs
|
|
@@ -412,7 +549,7 @@ A few things that are not obvious from the code but save a lot of confusion.
|
|
|
412
549
|
to stop already-queued runs, unless the rule-writing path withdraws them (which
|
|
413
550
|
provisioning does).
|
|
414
551
|
|
|
415
|
-
- **Each background pass runs on its own timer, with jitter.** The
|
|
552
|
+
- **Each background pass runs on its own timer, with jitter.** The passes are
|
|
416
553
|
independent and slightly randomised, so several servers do not all run their
|
|
417
554
|
passes in lockstep, and one slow pass does not stall the others.
|
|
418
555
|
|
|
@@ -420,10 +557,10 @@ A few things that are not obvious from the code but save a lot of confusion.
|
|
|
420
557
|
|
|
421
558
|
- **A dedicated host module.** Today the wiring that turns this pure library into a
|
|
422
559
|
running scheduler (DI, configuration, instance identity, event routing) lives
|
|
423
|
-
inside
|
|
424
|
-
`@n8n/scheduler-
|
|
425
|
-
|
|
426
|
-
|
|
560
|
+
inside n8n's large server package. Extracting it into its own module, for example
|
|
561
|
+
`@n8n/scheduler-features`, would keep the integration concerns together on their
|
|
562
|
+
own, and mirror the clean split this package already draws between algorithm and
|
|
563
|
+
host.
|
|
427
564
|
- **Lease renewal for long handlers.** A claim's lease is fixed for now, so a
|
|
428
565
|
handler that runs longer than its lease risks being recovered and re-run. A
|
|
429
566
|
heartbeat that extends the lease while a handler is genuinely still working would
|
|
@@ -433,14 +570,14 @@ A few things that are not obvious from the code but save a lot of confusion.
|
|
|
433
570
|
|
|
434
571
|
Pushed all the way, that host module becomes its own deployable worker. On a due
|
|
435
572
|
task it does not run the workflow, it creates an execution and enqueues a job (Bull
|
|
436
|
-
and Redis) for the regular workers, so the real work is decomposing
|
|
437
|
-
result is far leaner than
|
|
438
|
-
S3, and more). The worker only needs:
|
|
573
|
+
and Redis) for the regular workers, so the real work is decomposing the server
|
|
574
|
+
package. The result is far leaner than that package, which bundles every node's
|
|
575
|
+
dependencies (1Password, S3, and more). The worker only needs:
|
|
439
576
|
|
|
440
577
|
| Needs | Package | Pulls in |
|
|
441
578
|
| --- | --- | --- |
|
|
442
579
|
| Core logic | `@n8n/scheduler` | luxon, cron-parser |
|
|
443
|
-
| Task store + transactions | `@n8n/db` | `@n8n/typeorm` +
|
|
580
|
+
| Task store + transactions | `@n8n/db` | `@n8n/typeorm` + its driver |
|
|
444
581
|
| Enqueue executions (a new, extracted publisher) | `@n8n/…-publisher` | Bull + ioredis, execution writes, trigger-context |
|
|
445
582
|
| Logging + lifecycle | `@n8n/backend-common` | winston |
|
|
446
583
|
| Wiring + types | `@n8n/config`, `@n8n/di`, `@n8n/decorators`, `n8n-workflow` | reflect-metadata |
|