Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "clawflow",
"name": "ClawFlow",
"description": "The n8n for agents. Declarative, AI-native workflow engine — LLM-writable, Cloudflare-portable.",
"version": "1.2.9",
"version": "1.3.0",
"skills": ["./skills/clawflow"],
"activation": {
"onStartup": true
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clawnify/clawflow",
"version": "1.2.9",
"version": "1.3.0",
"description": "The n8n for agents. A declarative, AI-native workflow format that agents can read, write, and run.",
"type": "module",
"main": "./dist/index.js",
Expand Down
7 changes: 4 additions & 3 deletions skills/clawflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ A flow is JSON with a `flow` name, an optional `env` block, and a `nodes` array.
| Node | Purpose | Key fields |
|------|---------|------------|
| `ai` | Single LLM call, structured or freeform | `prompt`, `schema`, `model`, `input`, `attachments` |
| `agent` | Delegate to a real OpenClaw agent (with tools, browser, etc.) | `task`, `agentId`, `tools` |
| `agent` | Delegate to a real OpenClaw agent (with tools, browser, etc.) | `task`, `agentId`, `session`, `tools` |
| `exec` | Run a shell command deterministically (no AI) | `command`, `cwd` |
| `branch` | Multi-way routing with inline sub-flows per path | `on`, `paths`, `default` |
| `condition` | If/else with sub-node blocks that reconverge | `if`, `then`, `else` |
Expand All @@ -52,7 +52,8 @@ A flow is JSON with a `flow` name, an optional `env` block, and a `nodes` array.
- Use `do: exec` for deterministic operations (scripts, file processing, CLI tools) — never use `do: agent` for pure shell commands
- Use `do: agent` for tasks that need tools (browser, exec, memory, MCP, CLI) — delegates to a real OpenClaw agent
- Use `do: ai` for structured extraction and single-turn LLM calls
- Set `agentId: "clawflow"` on agent nodes to target a specific OpenClaw agent ID
- Set `agentId: "clawflow"` on agent nodes to target a configured OpenClaw agent (a plain slug like `main`/`clawflow`, never a session key)
- Set `session: "agent:main:slack:channel:agent"` on agent nodes to run inside a specific existing session (e.g. a channel) — maps to `openclaw agent --session-key`. An `agent:`-prefixed key is self-scoping; a bare key (e.g. `incident-42`) is scoped by `agentId`. May be combined with `agentId`
- Use `do: wait` with `for: approval` before any side effects that need human review — it pauses the flow, provides a token, and shows preview data to the approver
- Use `do: wait` with `for: event` to wait for external events (webhooks, signals)
- `do: condition` for boolean if/else, `do: branch` for multi-way value matching — both run inline sub-flows and reconverge
Expand Down Expand Up @@ -315,7 +316,7 @@ extract (agent) → parse (ai+schema) → loop:

### do: agent — exec approval setup

Agent nodes delegate to a real OpenClaw agent via `openclaw agent --agent <id> --message <task>`. By default, the spawned agent uses the "main" profile, which may prompt for exec approval on every shell command — blocking unattended flows.
Agent nodes delegate to a real OpenClaw agent via `openclaw agent --agent <id> --message <task>` (or `--session-key <key>` when you set `session` instead of `agentId`). By default, the spawned agent uses the "main" profile, which may prompt for exec approval on every shell command — blocking unattended flows.

To run agent nodes without interactive prompts, create a dedicated `clawflow` agent with full exec access:

Expand Down
18 changes: 14 additions & 4 deletions src/core/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,13 +976,14 @@ export class FlowRunner {
? parseDuration(node.timeout)
: undefined;

const cliResult = await this.tryOpenClawAgent(fullPrompt, node.agentId, state, timeoutMs);
const cliResult = await this.tryOpenClawAgent(fullPrompt, node.agentId, node.session, state, timeoutMs);
return { output: this.autoParseJson(cliResult) };
}

private async tryOpenClawAgent(
message: string,
agentId: string | undefined,
session: string | undefined,
state: FlowState,
nodeTimeoutMs?: number,
): Promise<string> {
Expand All @@ -997,9 +998,18 @@ export class FlowRunner {
throw new Error("openclaw CLI not found — agent nodes require the openclaw CLI to be installed");
}

// Resolution: node.agentId > plugin config defaultAgent > "main"
const effectiveAgent = agentId ?? this.cfg.defaultAgent ?? "main";
const args = ["agent", "--agent", effectiveAgent, "--message", message];
// Target selection mirrors `openclaw agent`'s own selectors, which compose:
// --agent <id> scopes to a configured agent
// --session-key <key> selects an exact session
// Per OpenClaw: a bare session key is scoped by --agent; an "agent:"-prefixed
// key must match --agent if both are given. So we pass through whichever are
// set and let OpenClaw enforce consistency, rather than re-asserting it here.
// When neither is set, fall back to a configured agent (defaultAgent > "main").
const args = ["agent"];
if (agentId) args.push("--agent", agentId);
if (session) args.push("--session-key", session);
if (!agentId && !session) args.push("--agent", this.cfg.defaultAgent ?? "main");
args.push("--message", message);

// Merge flow-level env vars (state.env) into the child process environment.
// Set CLAWFLOW_NO_SERVE to prevent the child from binding the webhook port.
Expand Down
13 changes: 11 additions & 2 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,19 @@ export interface AgentNode extends BaseNode {
task: string;
input?: string;
tools?: string[];
/** OpenClaw agent ID to delegate to (e.g. "main", "clawflow"). Uses OpenClaw's default routing if omitted. */
/** OpenClaw agent ID to delegate to (e.g. "main", "clawflow"). A plain slug, never a session key. Defaults to "main" if neither this nor `session` is set. */
agentId?: string;
/**
* OpenClaw session key to target, mapped to `openclaw agent --session-key`.
* Use this to run inside a specific existing session — a channel, a named
* agent session — rather than a fresh turn. "agent:"-prefixed keys (e.g.
* "agent:main:slack:channel:agent") are self-scoping; a bare key (e.g.
* "incident-42") is scoped by `agentId` (or the default agent). May be
* combined with `agentId` — OpenClaw requires them to be consistent.
*/
session?: string;
}
const AGENT_KEYS = ["task", "input", "tools", "agentId"] as const;
const AGENT_KEYS = ["task", "input", "tools", "agentId", "session"] as const;
const _agentCheck: CheckKeys<AgentNode, typeof AGENT_KEYS> = true;

export interface BranchNode extends BaseNode {
Expand Down
2 changes: 2 additions & 0 deletions src/core/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ function validateNodeFields(
const n = node as AgentNode;
if (!n.task) e("task", `agent node "${node.name}" requires "task"`);
if ("model" in node) e("model", `agent node "${node.name}" does not support "model" — configure the model on the openclaw agent instead`);
if (typeof n.agentId === "string" && n.agentId.includes(":"))
e("agentId", `agent node "${node.name}" has a session-key-shaped "agentId" ("${n.agentId}") — "agentId" is a plain agent slug (e.g. "main"). Use "session" for a session key like "agent:main:slack:channel:agent".`);
break;
}
case "branch": {
Expand Down
5 changes: 4 additions & 1 deletion src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,10 @@ State model:

Node types:
ai — LLM call, structured or freeform. Use schema: for typed output.
agent — open-ended autonomous task (falls back to high-capability AI)
agent — delegate to a real OpenClaw agent. Target with agentId (a configured
agent slug, e.g. "clawflow") and/or session (a session key, e.g.
"agent:main:slack:channel:agent" → --session-key). A bare session key
is scoped by agentId; an "agent:"-prefixed key is self-scoping.
branch — route to different nodes based on a value: { on, paths, default }
loop — iterate over a list: { over, as, nodes[] }
parallel — run nodes concurrently: { nodes[], mode: "all"|"race" }
Expand Down
47 changes: 47 additions & 0 deletions tests/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,53 @@ describe("validateFlow", () => {
const result = validateFlow(flow);
assert.equal(result.ok, true);
});

it("accepts an agent node with a session key", () => {
const flow: FlowDefinition = {
flow: "agent-session",
nodes: [{
name: "reply",
do: "agent" as const,
task: "answer",
session: "agent:main:slack:channel:agent",
output: "r",
}] as any,
};
const result = validateFlow(flow);
assert.equal(result.ok, true, JSON.stringify(result.errors));
});

it("rejects a session-key-shaped agentId (should use session)", () => {
const flow: FlowDefinition = {
flow: "agent-bad-id",
nodes: [{
name: "reply",
do: "agent" as const,
task: "answer",
agentId: "agent:main:slack:channel:agent",
output: "r",
}] as any,
};
const result = validateFlow(flow);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => e.message.includes("session-key-shaped")));
});

it("allows combining agentId with a bare session key (agent scopes the key)", () => {
const flow: FlowDefinition = {
flow: "agent-both",
nodes: [{
name: "reply",
do: "agent" as const,
task: "answer",
agentId: "ops",
session: "incident-42",
output: "r",
}] as any,
};
const result = validateFlow(flow);
assert.equal(result.ok, true, JSON.stringify(result.errors));
});
});

// ---- Attachments (multimodal) -------------------------------------------------------
Expand Down
Loading