Skip to content

feat: add n8n as a second workflow runtime alongside Node-RED#38

Closed
manihagh wants to merge 3 commits intomasterfrom
n8n-runtime
Closed

feat: add n8n as a second workflow runtime alongside Node-RED#38
manihagh wants to merge 3 commits intomasterfrom
n8n-runtime

Conversation

@manihagh
Copy link
Copy Markdown

Solutions whose chain-declared executionEnvironment is N8nV1 are now
installed into an embedded n8n child process. Solutions declared
NodeRedV1 or with the field empty continue to run on Node-RED exactly
as before (no behavior change).

The n8n runtime:

  • Spawns n8n 2.17.4 as a managed child process bound to 127.0.0.1
  • Waits for n8n's end-of-boot stdout marker before authenticating
  • Authenticates via generated worker-internal owner credentials stored
    at node-red-data/.n8n/owner-credentials.json
  • Creates workflows with embedded VCC tracking metadata
  • Activates via POST /rest/workflows/:id/activate with versionId
  • Deletes via archive-then-delete (n8n 2.17 requirement)

A new src/runtime/ directory introduces a Runtime interface with two
implementations (node-red, n8n). src/solution.ts, src/vote.ts,
src/worker-config.ts, and src/health.ts are now runtime-agnostic.

Includes:

  • Zod override to 3.25.67 to fix n8n api-types discriminated union
  • test-flows/vcc-1.json sanity flow
  • DEV_RUNTIME_OVERRIDES and DEV_LOCAL_FLOW_OVERRIDES for local iteration
  • RUNTIME-N8N.md with architecture, lifecycle, and operational notes

Verified end-to-end on mainnet: vcc-1 installed to n8n, scheduled
trigger fires every 30s, votes signed and settled on chain, confirmed
via VoteSubmitted event on workerNodePallet.

package-lock.json has ~43K lines of additions. This is expected from
introducing n8n as a dependency; no existing dependencies were removed
or downgraded.

No unit tests added - branch is verified by end-to-end testing.
Test scaffolding is a follow-up.

Solutions whose chain-declared executionEnvironment is N8nV1 are now
installed into an embedded n8n child process. Solutions declared
NodeRedV1 or with the field empty continue to run on Node-RED exactly
as before (no behavior change).

The n8n runtime:
- Spawns n8n 2.17.4 as a managed child process bound to 127.0.0.1
- Waits for n8n's end-of-boot stdout marker before authenticating
- Authenticates via generated worker-internal owner credentials stored
  at node-red-data/.n8n/owner-credentials.json
- Creates workflows with embedded VCC tracking metadata
- Activates via POST /rest/workflows/:id/activate with versionId
- Deletes via archive-then-delete (n8n 2.17 requirement)

A new src/runtime/ directory introduces a Runtime interface with two
implementations (node-red, n8n). src/solution.ts, src/vote.ts,
src/worker-config.ts, and src/health.ts are now runtime-agnostic.

Includes:
- Zod override to 3.25.67 to fix n8n api-types discriminated union
- test-flows/vcc-1.json sanity flow
- DEV_RUNTIME_OVERRIDES and DEV_LOCAL_FLOW_OVERRIDES for local iteration
- RUNTIME-N8N.md with architecture, lifecycle, and operational notes

Verified end-to-end on mainnet: vcc-1 installed to n8n, scheduled
trigger fires every 30s, votes signed and settled on chain, confirmed
via VoteSubmitted event on workerNodePallet.

package-lock.json has ~43K lines of additions. This is expected from
introducing n8n as a dependency; no existing dependencies were removed
or downgraded.

No unit tests added - branch is verified by end-to-end testing.
Test scaffolding is a follow-up.
@manihagh manihagh requested a review from hejkerooo as a code owner April 22, 2026 15:14
@manihagh manihagh requested review from azam-ismail, dwojno and kim-energyweb and removed request for dwojno April 22, 2026 15:15
Copy link
Copy Markdown
Collaborator

@hejkerooo hejkerooo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of running n8n as separate binary - wish we would've done the same for NodeRed if it's possible in current situation, it's something to be considered in future.

However this PR should've been created from this branch #34

It should be stable as of now, not sure why it's not merged yet.

const raw: string = readFileSync(credentialsPath, 'utf8');

try {
return JSON.parse(raw) as OwnerCredentials;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use zod validation for this to make sure that file is compatible with OwnerCredentials

};

writeFileSync(credentialsPath, JSON.stringify(credentials), { mode: 0o600 });
logger.info({ credentialsPath }, 'generated new n8n owner credentials');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to remove n8n keyword as logger has N8nRuntime context

let baseUrl: string | null = null;

/** Generate or load the encryption key n8n uses for its own credentials. */
const ensureEncryptionKey = (n8nUserDir: string): string => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const ensureEncryptionKey = (n8nUserDir: string): string => {
const createEncryptionKey = (n8nUserDir: string): string => {

};

/** Resolve path to the n8n CLI binary inside node_modules. */
const resolveN8nBinary = (): string => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const resolveN8nBinary = (): string => {
const resolveN8nBinaryPath = (): string => {

Comment thread src/main.ts Outdated
await deleteAll();
// Wipe any leftover installed flows from prior runs across every runtime
// that started successfully, so reconcile starts from a clean slate.
for (const rt of ALL_RUNTIMES) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we extract it as a function in runtime.ts?

Comment thread docs/runtime-n8n.md
@@ -0,0 +1,152 @@
# n8n runtime
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move this to docs/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

Comment thread src/solution.ts
@@ -1,17 +1,9 @@
import { type ApiPromise } from '@polkadot/api';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file will be reviewed once merge with feat/auth occurs

@@ -0,0 +1,24 @@
{
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread src/vote.ts Outdated
* directly. Returns null if the identifier matches nothing known.
*/
const resolveSolutionNamespace = async (
noderedId: string | undefined,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we extract it to interface? in case someone wanted to fetch with solutionId right now it would be

resolveSolutionNamespace(undefined, 'some-value');

which is not clear imo

// Idempotency: same solutionId at the same workLogic is a no-op.
const existingWorkflowId: string | undefined = installedBySolutionId.get(input.solutionId);

if (existingWorkflowId != null) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it's shared behavior, can we create a function that is shared with NR and N8N? some sort of default implementation

Addresses all review comments from #38:

- Move RUNTIME-N8N.md to docs/runtime-n8n.md
- Extract runtime bootstrap loops into startAllRuntimes and
  deleteAllFromAllRuntimes helpers in registry.ts
- Add getHealth() to the Runtime interface; remove NR special-casing
  in health.ts (which is now pure iteration over ALL_RUNTIMES)
- Replace resolveSolutionNamespace and resolveSolutionDetails awkward
  (noderedId?, solutionId?) signatures with a discriminated
  SolutionIdentifier type that makes intent explicit at call sites

Also cleans up lint issues surfaced during the refactor: nullish
coalescing in red.ts, unused sleep import, Promise constructor param
naming, type assertions in the flow-parse path.

No behavior change. TypeScript and eslint pass clean.
@manihagh manihagh requested a review from hejkerooo April 23, 2026 09:12
}, timeoutMs);
});

await Promise.race([n8nReadyPromise, timeout]);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to clear timeout, when n8nReadyPromise are return before timeout

@manihagh
Copy link
Copy Markdown
Author

Superseded by #41 — rebased onto v1.7.0 after #34 landed. All review feedback addressed in the new PR. Closing this one.

@manihagh manihagh closed this Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants