@n8n-as-code/n8nac 2026.4.1 → 2026.5.0-next.9

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 CHANGED
@@ -23,10 +23,12 @@ Restart the gateway, then run the setup wizard:
23
23
  openclaw n8nac:setup
24
24
  ```
25
25
 
26
- The wizard asks for your n8n host URL and API key once, saves them via
26
+ The wizard asks for your n8n host URL and API key once, saves an instance config via
27
27
  `n8nac init-auth`, selects your project, and generates an AI context file
28
28
  (`AGENTS.md`) in the workspace (`~/.openclaw/n8nac/`).
29
29
 
30
+ After setup, saved instance configs can also be listed, selected, and deleted through the same shared `n8nac` instance library used by the CLI and VS Code extension.
31
+
30
32
  ## Usage
31
33
 
32
34
  Once setup is done, just talk to OpenClaw:
@@ -63,7 +65,7 @@ All files live in `~/.openclaw/n8nac/`:
63
65
 
64
66
  ```
65
67
  ~/.openclaw/n8nac/
66
- n8nac-config.json ← project binding (written by n8nac init-project)
68
+ n8nac-config.json ← saved instance configs + active selection
67
69
  AGENTS.md ← AI context (written by n8nac update-ai)
68
70
  workflows/ ← .workflow.ts files (your n8n workflows)
69
71
  ```
@@ -75,8 +77,11 @@ The plugin registers the `n8nac` tool with these actions:
75
77
  | Action | Description |
76
78
  |---|---|
77
79
  | `setup_check` | Check initialization state |
78
- | `init_auth` | Save n8n credentials |
80
+ | `init_auth` | Save n8n credentials; pass `newInstance: true` to add another saved config |
79
81
  | `init_project` | Select n8n project |
82
+ | `instance_list` | List saved instance configs |
83
+ | `instance_select` | Select the active saved instance config |
84
+ | `instance_delete` | Delete a saved instance config |
80
85
  | `list` | List all workflows |
81
86
  | `pull` | Download a workflow by ID |
82
87
  | `push` | Upload a workflow file |
@@ -119,7 +124,7 @@ openclaw n8nac:setup
119
124
  ```
120
125
 
121
126
  Enter your n8n host and API key when prompted. The wizard writes
122
- `~/.openclaw/n8nac/n8nac-config.json` and generates `AGENTS.md`.
127
+ `~/.openclaw/n8nac/n8nac-config.json` with the saved instance configs and active selection, then generates `AGENTS.md`.
123
128
 
124
129
  ### 4. Iterate on the code
125
130
 
@@ -141,7 +146,7 @@ The plugin prefixes all `api.logger` calls with `[n8nac]`.
141
146
 
142
147
  ```
143
148
  ~/.openclaw/n8nac/
144
- n8nac-config.json ← written by init-project
149
+ n8nac-config.json ← saved instance configs + active selection
145
150
  AGENTS.md ← written by update-ai
146
151
  workflows/ ← .workflow.ts files
147
152
  ```
package/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { accessSync, constants, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
4
4
  import { registerN8nAcCli } from "./src/cli.js";
5
5
  import { createN8nAcTool } from "./src/tool.js";
6
- import { getWorkspaceDir, isWorkspaceInitialized } from "./src/workspace.js";
6
+ import { getWorkspaceDir, isWorkspaceInitialized, readWorkspaceBinding } from "./src/workspace.js";
7
7
 
8
8
  // ---------------------------------------------------------------------------
9
9
  // Lightweight prompt context
@@ -21,17 +21,8 @@ Once you have both, call the \`n8nac\` tool with \`action: "init_auth"\`, then
21
21
  \`action: "init_project"\` to finish setup.
22
22
  `;
23
23
 
24
- function readConfig(workspaceDir: string): Record<string, string> {
25
- try {
26
- const raw = readFileSync(join(workspaceDir, "n8nac-config.json"), "utf-8");
27
- return JSON.parse(raw) as Record<string, string>;
28
- } catch {
29
- return {};
30
- }
31
- }
32
-
33
24
  function buildStatusHeader(workspaceDir: string): string {
34
- const cfg = readConfig(workspaceDir);
25
+ const cfg = readWorkspaceBinding(workspaceDir);
35
26
  const host = cfg.host ?? "(unknown)";
36
27
  const project = cfg.projectName ?? cfg.projectId ?? "(unknown)";
37
28
  return [
@@ -40,6 +31,7 @@ function buildStatusHeader(workspaceDir: string): string {
40
31
  "**The workspace is already fully initialized. Do NOT ask the user for credentials.**",
41
32
  "",
42
33
  `- Workspace directory: \`${workspaceDir}\``,
34
+ `- Active instance: \`${cfg.activeInstanceName ?? cfg.activeInstanceId ?? "(unknown)"}\``,
43
35
  `- n8n host: \`${host}\``,
44
36
  `- Active project: \`${project}\``,
45
37
  ].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n8n-as-code/n8nac",
3
- "version": "2026.4.1",
3
+ "version": "2026.5.0-next.9",
4
4
  "description": "OpenClaw plugin for n8n-as-code — create and manage n8n workflows from OpenClaw",
5
5
  "keywords": [
6
6
  "n8n",
@@ -12,11 +12,12 @@ Use this skill only for explicit n8n workflow work.
12
12
  1. Check whether `n8nac-config.json` exists in the workspace root.
13
13
  2. If the workspace is initialized, read `AGENTS.md` from the workspace root before making workflow changes. It is the detailed, workspace-specific source of truth generated by `n8nac update-ai`.
14
14
  3. If `AGENTS.md` is missing or unreadable, regenerate it with `npx --yes n8nac update-ai` or run the `openclaw n8nac:setup` command before attempting workflow authoring.
15
- 4. If the workspace is not initialized, ask the user for the n8n host URL and API key, then use the `n8nac` tool with `action: "init_auth"` and `action: "init_project"` to complete setup yourself.
15
+ 4. If the workspace is not initialized, ask the user for the n8n host URL and API key, then use the `n8nac` tool with `action: "init_auth"` and `action: "init_project"` to complete setup yourself. If you need to add a second saved instance later, call `action: "init_auth"` with `newInstance: true` first.
16
16
 
17
17
  ## Using the n8nac tool
18
18
 
19
- - Use the `n8nac` tool for setup checks, workflow list/pull/push/verify, validation, and `skills` lookups.
19
+ - Use the `n8nac` tool for setup checks, saved instance config management, workflow list/pull/push/verify, validation, and `skills` lookups.
20
+ - Use `action: "instance_list"` to inspect saved configs, `action: "instance_select"` to switch the active config, and `action: "instance_delete"` to remove a stale saved config.
20
21
  - Use `action: "skills"` whenever you need node search or schema details.
21
22
  - Never guess node parameters. The schema lookup is the source of truth.
22
23
  - Treat `AGENTS.md` as the authoritative workflow-engineering protocol once this skill is active.
package/src/tool.ts CHANGED
@@ -11,6 +11,9 @@ const ACTIONS = [
11
11
  "setup_check",
12
12
  "init_auth",
13
13
  "init_project",
14
+ "instance_list",
15
+ "instance_select",
16
+ "instance_delete",
14
17
  "list",
15
18
  "pull",
16
19
  "push",
@@ -28,8 +31,11 @@ const N8nAcToolSchema = Type.Object({
28
31
  description: [
29
32
  "Action to perform:",
30
33
  " setup_check — check whether the workspace is initialized.",
31
- " init_auth — save n8n credentials. Requires n8nHost and n8nApiKey.",
34
+ " init_auth — save n8n credentials. Requires n8nHost and n8nApiKey. Pass newInstance: true to add another saved config.",
32
35
  " init_project — select the n8n project. Optionally pass projectId, projectName, or projectIndex (1-based, default 1).",
36
+ " instance_list — list saved n8n instance configs as JSON.",
37
+ " instance_select — switch the active saved instance config. Requires instanceId, instanceName, or instanceIndex.",
38
+ " instance_delete — delete a saved instance config. Requires instanceId, instanceName, or instanceIndex.",
33
39
  " list — list all workflows with their sync status.",
34
40
  " pull — download a workflow from n8n. Requires workflowId.",
35
41
  " push — upload a local workflow file. Requires filename (e.g. my-flow.workflow.ts).",
@@ -43,12 +49,17 @@ const N8nAcToolSchema = Type.Object({
43
49
  Type.String({ description: "n8n host URL (for init_auth). Example: https://your-n8n.example.com" }),
44
50
  ),
45
51
  n8nApiKey: Type.Optional(Type.String({ description: "n8n API key (for init_auth)" })),
52
+ newInstance: Type.Optional(Type.Boolean({ description: "Save credentials as a new saved instance config instead of updating the current one (for init_auth)." })),
46
53
  // init_project
47
54
  projectId: Type.Optional(Type.String({ description: "n8n project ID (for init_project)" })),
48
55
  projectName: Type.Optional(Type.String({ description: "n8n project name (for init_project)" })),
49
56
  projectIndex: Type.Optional(
50
57
  Type.Number({ description: "n8n project index, 1-based (for init_project, default: 1)" }),
51
58
  ),
59
+ // instance_select / instance_delete
60
+ instanceId: Type.Optional(Type.String({ description: "Saved instance config ID (for instance_select, instance_delete)." })),
61
+ instanceName: Type.Optional(Type.String({ description: "Saved instance config name (for instance_select, instance_delete)." })),
62
+ instanceIndex: Type.Optional(Type.Number({ description: "Saved instance config index, 1-based (for instance_select, instance_delete)." })),
52
63
  listScope: Type.Optional(
53
64
  Type.Unsafe<(typeof LIST_SCOPES)[number]>({
54
65
  type: "string",
@@ -229,7 +240,7 @@ export function createN8nAcTool(opts: { workspaceDir: string }) {
229
240
  label: "n8n-as-code",
230
241
  description:
231
242
  "Create and manage n8n workflows using n8n-as-code. " +
232
- "Handles workspace initialization (init_auth → init_project), " +
243
+ "Handles workspace initialization (init_auth → init_project), saved instance config management, " +
233
244
  "workflow sync (list, pull, push, verify), and AI knowledge lookup (skills, validate). " +
234
245
  "Always call setup_check first to determine initialization state.",
235
246
  parameters: N8nAcToolSchema,
@@ -244,7 +255,7 @@ export function createN8nAcTool(opts: { workspaceDir: string }) {
244
255
  initialized,
245
256
  workspaceDir,
246
257
  next: initialized
247
- ? "Workspace is ready. Use list, pull, push, verify, or skills."
258
+ ? "Workspace is ready. Use instance_list, instance_select, list, pull, push, verify, or skills."
248
259
  : "Workspace not initialized. Ask the user for their n8n host URL and API key, then call init_auth.",
249
260
  });
250
261
  }
@@ -256,7 +267,11 @@ export function createN8nAcTool(opts: { workspaceDir: string }) {
256
267
  if (!host || !key) {
257
268
  return ok({ error: "n8nHost and n8nApiKey are required for init_auth" });
258
269
  }
259
- const r = await runNpx(["init-auth", "--host", host, "--api-key-stdin"], workspaceDir, key);
270
+ const args = ["init-auth", "--host", host, "--api-key-stdin"];
271
+ if (params.newInstance === true) {
272
+ args.push("--new-instance");
273
+ }
274
+ const r = await runNpx(args, workspaceDir, key);
260
275
  if (r.exitCode !== 0) {
261
276
  return ok({ error: r.stderr || r.stdout, exitCode: r.exitCode });
262
277
  }
@@ -299,6 +314,48 @@ export function createN8nAcTool(opts: { workspaceDir: string }) {
299
314
  });
300
315
  }
301
316
 
317
+ // ---- instance_list -----------------------------------------------
318
+ if (action === "instance_list") {
319
+ const r = await runNpx(["instance", "list", "--json"], workspaceDir);
320
+ return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
321
+ }
322
+
323
+ // ---- instance_select ---------------------------------------------
324
+ if (action === "instance_select") {
325
+ const instanceId = str(params.instanceId);
326
+ const instanceName = str(params.instanceName);
327
+ const instanceIndex = typeof params.instanceIndex === "number" ? params.instanceIndex : undefined;
328
+ if (!instanceId && !instanceName && instanceIndex === undefined) {
329
+ return ok({ error: "instanceId, instanceName, or instanceIndex is required for instance_select" });
330
+ }
331
+
332
+ const args = ["instance", "select"];
333
+ if (instanceId) args.push("--instance-id", instanceId);
334
+ else if (instanceName) args.push("--instance-name", instanceName);
335
+ else args.push("--instance-index", String(instanceIndex));
336
+
337
+ const r = await runNpx(args, workspaceDir);
338
+ return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
339
+ }
340
+
341
+ // ---- instance_delete ---------------------------------------------
342
+ if (action === "instance_delete") {
343
+ const instanceId = str(params.instanceId);
344
+ const instanceName = str(params.instanceName);
345
+ const instanceIndex = typeof params.instanceIndex === "number" ? params.instanceIndex : undefined;
346
+ if (!instanceId && !instanceName && instanceIndex === undefined) {
347
+ return ok({ error: "instanceId, instanceName, or instanceIndex is required for instance_delete" });
348
+ }
349
+
350
+ const args = ["instance", "delete", "--yes"];
351
+ if (instanceId) args.push("--instance-id", instanceId);
352
+ else if (instanceName) args.push("--instance-name", instanceName);
353
+ else args.push("--instance-index", String(instanceIndex));
354
+
355
+ const r = await runNpx(args, workspaceDir);
356
+ return ok({ exitCode: r.exitCode, output: r.stdout, error: r.stderr || undefined });
357
+ }
358
+
302
359
  // ---- list ---------------------------------------------------------
303
360
  if (action === "list") {
304
361
  const scope = str(params.listScope) || "all";
package/src/workspace.ts CHANGED
@@ -2,6 +2,15 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
5
+ export type WorkspaceBinding = {
6
+ host?: string;
7
+ projectId?: string;
8
+ projectName?: string;
9
+ syncFolder?: string;
10
+ activeInstanceId?: string;
11
+ activeInstanceName?: string;
12
+ };
13
+
5
14
  /**
6
15
  * Fixed workspace directory for V1.
7
16
  * All n8nac files (n8nac-config.json, AGENTS.md, workflows/) live here.
@@ -10,22 +19,63 @@ export function getWorkspaceDir(): string {
10
19
  return join(homedir(), ".openclaw", "n8nac");
11
20
  }
12
21
 
13
- /**
14
- * Returns true when n8nac has been initialized in the given directory,
15
- * meaning the config exists and contains a selected project + sync folder.
16
- */
17
- export function isWorkspaceInitialized(workspaceDir: string): boolean {
22
+ function readString(value: unknown): string {
23
+ return typeof value === "string" ? value.trim() : "";
24
+ }
25
+
26
+ function resolveActiveInstance(config: Record<string, unknown>): Record<string, unknown> | undefined {
27
+ if (!Array.isArray(config.instances)) {
28
+ return undefined;
29
+ }
30
+
31
+ const instances = config.instances.filter((value): value is Record<string, unknown> => !!value && typeof value === "object");
32
+ if (!instances.length) {
33
+ return undefined;
34
+ }
35
+
36
+ const activeInstanceId = readString(config.activeInstanceId);
37
+ if (activeInstanceId) {
38
+ return instances.find((instance) => readString(instance.id) === activeInstanceId) || instances[0];
39
+ }
40
+
41
+ return instances[0];
42
+ }
43
+
44
+ export function readWorkspaceBinding(workspaceDir: string): WorkspaceBinding {
18
45
  const configPath = join(workspaceDir, "n8nac-config.json");
19
- if (!existsSync(configPath)) return false;
46
+ if (!existsSync(configPath)) {
47
+ return {};
48
+ }
20
49
 
21
50
  try {
22
51
  const raw = readFileSync(configPath, "utf-8");
23
52
  const config = JSON.parse(raw) as Record<string, unknown>;
24
- const projectId = typeof config.projectId === "string" ? config.projectId.trim() : "";
25
- const projectName = typeof config.projectName === "string" ? config.projectName.trim() : "";
26
- const syncFolder = typeof config.syncFolder === "string" ? config.syncFolder.trim() : "";
27
- return projectId.length > 0 && projectName.length > 0 && syncFolder.length > 0;
53
+ const activeInstance = resolveActiveInstance(config);
54
+ const configActiveInstanceId = readString(config.activeInstanceId);
55
+ const resolvedActiveInstanceId = readString(activeInstance?.id);
56
+ const activeInstanceId =
57
+ resolvedActiveInstanceId && resolvedActiveInstanceId === configActiveInstanceId
58
+ ? configActiveInstanceId
59
+ : resolvedActiveInstanceId || undefined;
60
+
61
+ return {
62
+ host: readString(activeInstance?.host) || readString(config.host) || undefined,
63
+ projectId: readString(activeInstance?.projectId) || readString(config.projectId) || undefined,
64
+ projectName: readString(activeInstance?.projectName) || readString(config.projectName) || undefined,
65
+ syncFolder: readString(activeInstance?.syncFolder) || readString(config.syncFolder) || undefined,
66
+ activeInstanceId,
67
+ activeInstanceName: readString(activeInstance?.name) || undefined,
68
+ };
28
69
  } catch {
29
- return false;
70
+ return {};
30
71
  }
31
72
  }
73
+
74
+ /**
75
+ * Returns true when n8nac has been initialized in the given directory,
76
+ * meaning the config exists and contains a selected project + sync folder.
77
+ */
78
+ export function isWorkspaceInitialized(workspaceDir: string): boolean {
79
+ const binding = readWorkspaceBinding(workspaceDir);
80
+ return Boolean(binding.projectId && binding.projectName && binding.syncFolder);
81
+ }