@kanvas/openclaw-plugin 0.1.17 → 0.1.19

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
@@ -1,6 +1,6 @@
1
1
  # Kanvas OpenClaw Plugin
2
2
 
3
- OpenClaw plugin that connects AI agents to [Kanvas](https://kanvas.dev) your company's nervous system. Kanvas is the operational engine that connects all your data, tools, and workflows. For AI agents, it's what lets them actually run your business — not just talk about it.
3
+ OpenClaw plugin that connects AI agents to [Kanvas](https://kanvas.dev) your company's nervous system. Kanvas is the operational engine that connects all your data, tools, and workflows. For AI agents, it's what lets them actually run your business — not just talk about it.
4
4
 
5
5
  This plugin gives agents direct access to CRM, inventory, orders, and messaging — 53 tools with auto-login, built-in system prompt context, and domain-specific skills.
6
6
 
@@ -0,0 +1,203 @@
1
+ const PLAN_DETAIL_FIELDS = `
2
+ id
3
+ uuid
4
+ title
5
+ description
6
+ plan_type
7
+ status
8
+ priority
9
+ deadline_at
10
+ completion_pct
11
+ requires_human_approval
12
+ approved_at
13
+ review_outcome
14
+ started_at
15
+ completed_at
16
+ error_message
17
+ input
18
+ output
19
+ confidence_score
20
+ entity_namespace
21
+ entity_id
22
+ created_at
23
+ updated_at
24
+ agent { id name }
25
+ user { id firstname lastname }
26
+ approver { id firstname lastname }
27
+ parent { id title status }
28
+ tasks { id sequence title description status blocked_reason result started_at completed_at }
29
+ files { data { id uuid name url } }
30
+ tags { data { id name } }
31
+ `;
32
+ const PLAN_LIST_FIELDS = `
33
+ id
34
+ uuid
35
+ title
36
+ description
37
+ plan_type
38
+ status
39
+ priority
40
+ deadline_at
41
+ completion_pct
42
+ requires_human_approval
43
+ entity_namespace
44
+ entity_id
45
+ created_at
46
+ agent { id name }
47
+ tasks { id sequence title status blocked_reason }
48
+ `;
49
+ const TASK_FIELDS = `
50
+ id
51
+ uuid
52
+ sequence
53
+ title
54
+ description
55
+ status
56
+ blocked_reason
57
+ result
58
+ started_at
59
+ completed_at
60
+ created_at
61
+ updated_at
62
+ `;
63
+ export class NervousSystemService {
64
+ client;
65
+ constructor(client) {
66
+ this.client = client;
67
+ }
68
+ // ── Read ───────────────────────────────────────────────────
69
+ async listMyPlans(opts) {
70
+ const { agent_id, statuses, first = 50 } = opts;
71
+ const where = statuses && statuses.length
72
+ ? {
73
+ AND: [
74
+ { column: "AGENT_ID", operator: "EQ", value: agent_id },
75
+ { column: "STATUS", operator: "IN", value: statuses },
76
+ ],
77
+ }
78
+ : { column: "AGENT_ID", operator: "EQ", value: agent_id };
79
+ const query = `
80
+ query MyPlans($first: Int!, $where: QueryNervousSystemPlansWhereWhereConditions) {
81
+ nervousSystemPlans(
82
+ first: $first
83
+ where: $where
84
+ orderBy: [{ column: PRIORITY, order: DESC }, { column: DEADLINE_AT, order: ASC }]
85
+ ) {
86
+ data { ${PLAN_LIST_FIELDS} }
87
+ paginatorInfo { currentPage lastPage total }
88
+ }
89
+ }
90
+ `;
91
+ return this.client.query(query, { first, where });
92
+ }
93
+ async listPlans(opts = {}) {
94
+ const { first = 25, where, orderBy } = opts;
95
+ const query = `
96
+ query ListPlans($first: Int!, $where: QueryNervousSystemPlansWhereWhereConditions, $orderBy: [QueryNervousSystemPlansOrderByOrderByClause!]) {
97
+ nervousSystemPlans(first: $first, where: $where, orderBy: $orderBy) {
98
+ data { ${PLAN_LIST_FIELDS} }
99
+ paginatorInfo { currentPage lastPage total }
100
+ }
101
+ }
102
+ `;
103
+ return this.client.query(query, { first, where, orderBy });
104
+ }
105
+ async getPlan(id) {
106
+ const query = `
107
+ query GetPlan($id: ID!) {
108
+ nervousSystemPlan(id: $id) {
109
+ ${PLAN_DETAIL_FIELDS}
110
+ }
111
+ }
112
+ `;
113
+ return this.client.query(query, { id });
114
+ }
115
+ async getAgentCapabilities(agentId, framework) {
116
+ const query = `
117
+ query AgentCapabilities($agent_id: ID!, $framework: String) {
118
+ nervousSystemAgentCapabilities(agent_id: $agent_id, framework: $framework) {
119
+ skills {
120
+ id name description skill_type version is_active
121
+ }
122
+ tools {
123
+ id name description tool_type requires_permission version is_active
124
+ }
125
+ }
126
+ }
127
+ `;
128
+ return this.client.query(query, { agent_id: agentId, framework });
129
+ }
130
+ // ── Plan mutations ─────────────────────────────────────────
131
+ async createPlan(input) {
132
+ const mutation = `
133
+ mutation CreatePlan($input: CreateNervousSystemPlanInput!) {
134
+ createNervousSystemPlan(input: $input) {
135
+ ${PLAN_DETAIL_FIELDS}
136
+ }
137
+ }
138
+ `;
139
+ return this.client.query(mutation, { input });
140
+ }
141
+ async updatePlan(id, input) {
142
+ const mutation = `
143
+ mutation UpdatePlan($id: ID!, $input: UpdateNervousSystemPlanInput!) {
144
+ updateNervousSystemPlan(id: $id, input: $input) {
145
+ ${PLAN_DETAIL_FIELDS}
146
+ }
147
+ }
148
+ `;
149
+ return this.client.query(mutation, { id, input });
150
+ }
151
+ async approvePlan(id, input) {
152
+ const mutation = `
153
+ mutation ApprovePlan($id: ID!, $input: ApproveNervousSystemPlanInput!) {
154
+ approveNervousSystemPlan(id: $id, input: $input) {
155
+ id
156
+ status
157
+ requires_human_approval
158
+ approved_at
159
+ review_outcome
160
+ approver { id firstname lastname }
161
+ }
162
+ }
163
+ `;
164
+ return this.client.query(mutation, { id, input });
165
+ }
166
+ async deletePlan(id) {
167
+ const mutation = `
168
+ mutation DeletePlan($id: ID!) {
169
+ deleteNervousSystemPlan(id: $id)
170
+ }
171
+ `;
172
+ return this.client.query(mutation, { id });
173
+ }
174
+ // ── Task mutations ─────────────────────────────────────────
175
+ async addTask(planId, input) {
176
+ const mutation = `
177
+ mutation AddTask($plan_id: ID!, $input: NervousSystemTaskInput!) {
178
+ addTaskToNervousSystemPlan(plan_id: $plan_id, input: $input) {
179
+ ${TASK_FIELDS}
180
+ }
181
+ }
182
+ `;
183
+ return this.client.query(mutation, { plan_id: planId, input });
184
+ }
185
+ async updateTaskStatus(id, input) {
186
+ const mutation = `
187
+ mutation UpdateTaskStatus($id: ID!, $input: UpdateNervousSystemTaskStatusInput!) {
188
+ updateNervousSystemTaskStatus(id: $id, input: $input) {
189
+ ${TASK_FIELDS}
190
+ }
191
+ }
192
+ `;
193
+ return this.client.query(mutation, { id, input });
194
+ }
195
+ async deleteTask(id) {
196
+ const mutation = `
197
+ mutation DeleteTask($id: ID!) {
198
+ deleteNervousSystemTask(id: $id)
199
+ }
200
+ `;
201
+ return this.client.query(mutation, { id });
202
+ }
203
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { SocialService } from "./domains/social/index.js";
7
7
  import { EcosystemService } from "./domains/ecosystem/index.js";
8
8
  import { DealsService } from "./domains/deals/index.js";
9
9
  import { EventsService } from "./domains/events/index.js";
10
+ import { NervousSystemService } from "./domains/nervousSystem/index.js";
10
11
  import { registerCrmTools } from "./tools/crm.js";
11
12
  import { registerInventoryTools } from "./tools/inventory.js";
12
13
  import { registerOrdersTools } from "./tools/orders.js";
@@ -14,6 +15,7 @@ import { registerSocialTools } from "./tools/social.js";
14
15
  import { registerEcosystemTools } from "./tools/ecosystem.js";
15
16
  import { registerDealsTools } from "./tools/deals.js";
16
17
  import { registerEventsTools } from "./tools/events.js";
18
+ import { registerNervousSystemTools } from "./tools/nervousSystem.js";
17
19
  import { toolResult } from "./tools/helpers.js";
18
20
  const DEFAULT_API_URL = "https://graphapi.kanvas.dev/graphql";
19
21
  // OpenClaw v2026.4.5+ calls register() per-agent-context (main, subagents,
@@ -29,6 +31,7 @@ let sharedSocial = null;
29
31
  let sharedEcosystem = null;
30
32
  let sharedDeals = null;
31
33
  let sharedEvents = null;
34
+ let sharedNervousSystem = null;
32
35
  let startupBannerShown = false;
33
36
  let skipBannerShown = false;
34
37
  function resolveConfig(pluginConfig) {
@@ -123,6 +126,7 @@ export default {
123
126
  sharedEcosystem = new EcosystemService(sharedClient);
124
127
  sharedDeals = new DealsService(sharedClient);
125
128
  sharedEvents = new EventsService(sharedClient);
129
+ sharedNervousSystem = new NervousSystemService(sharedClient);
126
130
  }
127
131
  // Tools and hooks must be registered on every api object — each one is
128
132
  // a separate agent context (main, subagents, cron lanes).
@@ -134,6 +138,7 @@ export default {
134
138
  registerEcosystemTools(api, sharedEcosystem, ensureAuth);
135
139
  registerDealsTools(api, sharedDeals, ensureAuth);
136
140
  registerEventsTools(api, sharedEvents, ensureAuth);
141
+ registerNervousSystemTools(api, sharedNervousSystem, ensureAuth);
137
142
  api.registerTool({
138
143
  name: "kanvas_test_connection",
139
144
  label: "Test Connection",
@@ -158,7 +163,7 @@ export default {
158
163
  }, { commands: ["setup"] });
159
164
  if (!startupBannerShown) {
160
165
  startupBannerShown = true;
161
- api.logger.info("Kanvas plugin registered — 73 tools loaded");
166
+ api.logger.info("Kanvas plugin registered — 84 tools loaded");
162
167
  }
163
168
  },
164
169
  };
@@ -279,6 +284,21 @@ Use when the user asks about companies, branches/locations, user management, rol
279
284
  - \`kanvas_assign_role_to_user\` → assign roles (e.g. super admin). Call \`kanvas_list_roles\` first to get role IDs.
280
285
  - \`kanvas_remove_role_from_user\` → remove a role from a user
281
286
 
287
+ **Nervous System (Plans, Tasks, Activities)**
288
+ This is how humans assign work to you. Read the \`nervous-system-working\` skill for the full protocol. Treat every non-trivial request as a Plan; plan first, execute second. Move statuses deliberately — humans watch the kanban.
289
+ - \`kanvas_list_my_plans\` → find work assigned to you (filter by statuses: draft/awaiting_approval/active/blocked)
290
+ - \`kanvas_get_plan\` → full plan detail (description, input, tasks, files, parent)
291
+ - \`kanvas_create_plan\` → create plan or sub-plan, optionally with seeded tasks
292
+ - \`kanvas_update_plan\` → transition status (draft→active→done/blocked/failed), set output JSON, attach files
293
+ - \`kanvas_approve_plan\` → approve/reject a plan in awaiting_approval
294
+ - \`kanvas_add_task\` → append a task to the checklist (sequence determines order)
295
+ - \`kanvas_update_task_status\` → pending → in_progress → done (or blocked/failed)
296
+ - \`kanvas_list_agent_capabilities\` → list skills/tools granted to an agent
297
+ - \`kanvas_delete_plan\` / \`kanvas_delete_task\` → delete
298
+ - For Activities (the conversation channel), use \`kanvas_create_message\` with \`channel_slug = plan.uuid\` and \`message_verb: "comment"\`. Read with \`kanvas_list_channel_messages\` using the same slug.
299
+ - **Always re-read the channel** before acting on a plan you've worked on before.
300
+ - **Never act** on a plan with \`requires_human_approval=true\` until it's flipped to active.
301
+
282
302
  **Diagnostics**
283
303
  - \`kanvas_test_connection\` → verify the API is reachable
284
304
 
@@ -0,0 +1,217 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { toolResult } from "./helpers.js";
3
+ const TaskInputSchema = Type.Object({
4
+ title: Type.String({ description: "Task title (verb phrase, e.g. 'Crawl /features')" }),
5
+ sequence: Type.Optional(Type.Number({ description: "Execution order (0, 1, 2...)" })),
6
+ description: Type.Optional(Type.String()),
7
+ status: Type.Optional(Type.String({ description: "pending | in_progress | blocked | done | failed | cancelled" })),
8
+ result: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
9
+ blocked_reason: Type.Optional(Type.String()),
10
+ });
11
+ const FilesInputSchema = Type.Array(Type.Object({
12
+ url: Type.String({ description: "Public URL of the file" }),
13
+ name: Type.String({ description: "Display name" }),
14
+ field_name: Type.Optional(Type.String()),
15
+ }), { description: "Files to attach (appended, not replaced)" });
16
+ const PlanStatusSchema = Type.String({
17
+ description: "Plan status: draft | awaiting_approval | active | blocked | done | failed | cancelled",
18
+ });
19
+ const TaskStatusSchema = Type.String({
20
+ description: "Task status: pending | in_progress | blocked | done | failed | cancelled",
21
+ });
22
+ export function registerNervousSystemTools(api, service, ensureAuth) {
23
+ // ── Read ───────────────────────────────────────────────────
24
+ api.registerTool({
25
+ name: "kanvas_list_my_plans",
26
+ label: "List My Plans",
27
+ description: "List nervous-system plans assigned to an agent. Filter by statuses to see open work " +
28
+ "(e.g. ['draft','awaiting_approval','active','blocked']). Returns plans ordered by priority desc, deadline asc.",
29
+ parameters: Type.Object({
30
+ agent_id: Type.Union([Type.String(), Type.Number()], { description: "The agent's ID" }),
31
+ statuses: Type.Optional(Type.Array(Type.String(), {
32
+ description: "Statuses to include. Default: all.",
33
+ })),
34
+ first: Type.Optional(Type.Number({ description: "Max results (default 50)" })),
35
+ }),
36
+ async execute(_id, params) {
37
+ await ensureAuth();
38
+ return toolResult(await service.listMyPlans(params));
39
+ },
40
+ });
41
+ api.registerTool({
42
+ name: "kanvas_list_plans",
43
+ label: "List Plans (admin)",
44
+ description: "List nervous-system plans with arbitrary where filters. Allowed where columns: " +
45
+ "ID, UUID, AGENT_ID, USERS_ID, ENTITY_NAMESPACE, ENTITY_ID, PLAN_TYPE, STATUS, PRIORITY, PARENT_PLAN_ID.",
46
+ parameters: Type.Object({
47
+ first: Type.Optional(Type.Number({ description: "Max results (default 25)" })),
48
+ where: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
49
+ orderBy: Type.Optional(Type.Array(Type.Object({
50
+ column: Type.String(),
51
+ order: Type.String({ description: "ASC or DESC" }),
52
+ }))),
53
+ }),
54
+ async execute(_id, params) {
55
+ await ensureAuth();
56
+ return toolResult(await service.listPlans(params));
57
+ },
58
+ });
59
+ api.registerTool({
60
+ name: "kanvas_get_plan",
61
+ label: "Get Plan",
62
+ description: "Get full plan detail: description, input/output, tasks, files, tags, agent, user, parent, completion %.",
63
+ parameters: Type.Object({
64
+ id: Type.String({ description: "Plan ID" }),
65
+ }),
66
+ async execute(_id, params) {
67
+ await ensureAuth();
68
+ return toolResult(await service.getPlan(params.id));
69
+ },
70
+ });
71
+ api.registerTool({
72
+ name: "kanvas_list_agent_capabilities",
73
+ label: "List Agent Capabilities",
74
+ description: "List the skills and tools an agent has been granted. Use this to verify you can do something before agreeing to a plan.",
75
+ parameters: Type.Object({
76
+ agent_id: Type.String({ description: "Agent ID" }),
77
+ framework: Type.Optional(Type.String({ description: "Framework filter (optional)" })),
78
+ }),
79
+ async execute(_id, params) {
80
+ await ensureAuth();
81
+ return toolResult(await service.getAgentCapabilities(params.agent_id, params.framework));
82
+ },
83
+ });
84
+ // ── Plan write ─────────────────────────────────────────────
85
+ api.registerTool({
86
+ name: "kanvas_create_plan",
87
+ label: "Create Plan",
88
+ description: "Create a nervous-system plan (or sub-plan when parent_plan_id is provided). " +
89
+ "Include a tasks array to seed the checklist on creation. For sensitive work, set requires_human_approval=true " +
90
+ "to gate the plan in awaiting_approval status until a human approves.",
91
+ parameters: Type.Object({
92
+ title: Type.String(),
93
+ plan_type: Type.String({ description: 'e.g. "data_migration", "outreach", "report_generation"' }),
94
+ description: Type.Optional(Type.String()),
95
+ agent_id: Type.Optional(Type.Number()),
96
+ users_id: Type.Optional(Type.Number()),
97
+ parent_plan_id: Type.Optional(Type.Number()),
98
+ entity_namespace: Type.Optional(Type.String({ description: "FQCN of the related entity (e.g. App\\\\Models\\\\Lead)" })),
99
+ entity_id: Type.Optional(Type.Number()),
100
+ status: Type.Optional(PlanStatusSchema),
101
+ priority: Type.Optional(Type.Number({ description: "Higher = more important" })),
102
+ deadline_at: Type.Optional(Type.String({ description: "ISO datetime" })),
103
+ input: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
104
+ output: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
105
+ confidence_score: Type.Optional(Type.Number()),
106
+ requires_human_approval: Type.Optional(Type.Boolean()),
107
+ tasks: Type.Optional(Type.Array(TaskInputSchema)),
108
+ files: Type.Optional(FilesInputSchema),
109
+ }),
110
+ async execute(_id, params) {
111
+ await ensureAuth();
112
+ return toolResult(await service.createPlan(params));
113
+ },
114
+ });
115
+ api.registerTool({
116
+ name: "kanvas_update_plan",
117
+ label: "Update Plan",
118
+ description: "Update a plan's status, title, description, output, or attach files. " +
119
+ "Use status transitions to signal progress: draft→active to start, active→blocked when stuck, active→done/failed when finished.",
120
+ parameters: Type.Object({
121
+ id: Type.String({ description: "Plan ID" }),
122
+ title: Type.Optional(Type.String()),
123
+ description: Type.Optional(Type.String()),
124
+ status: Type.Optional(PlanStatusSchema),
125
+ priority: Type.Optional(Type.Number()),
126
+ deadline_at: Type.Optional(Type.String()),
127
+ input: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
128
+ output: Type.Optional(Type.Record(Type.String(), Type.Unknown()), {
129
+ description: "Final output JSON (set this before flipping to done)",
130
+ }),
131
+ confidence_score: Type.Optional(Type.Number()),
132
+ requires_human_approval: Type.Optional(Type.Boolean()),
133
+ files: Type.Optional(FilesInputSchema),
134
+ }),
135
+ async execute(_id, params) {
136
+ await ensureAuth();
137
+ const { id, ...input } = params;
138
+ return toolResult(await service.updatePlan(id, input));
139
+ },
140
+ });
141
+ api.registerTool({
142
+ name: "kanvas_approve_plan",
143
+ label: "Approve Plan",
144
+ description: "Approve or reject a plan that is in awaiting_approval status. Approving flips it to active; rejecting flips it to cancelled.",
145
+ parameters: Type.Object({
146
+ id: Type.String({ description: "Plan ID" }),
147
+ approved: Type.Boolean({ description: "true to approve, false to reject" }),
148
+ review_outcome: Type.Optional(Type.String({ description: "Optional reviewer note" })),
149
+ }),
150
+ async execute(_id, params) {
151
+ await ensureAuth();
152
+ return toolResult(await service.approvePlan(params.id, {
153
+ approved: params.approved,
154
+ review_outcome: params.review_outcome,
155
+ }));
156
+ },
157
+ });
158
+ api.registerTool({
159
+ name: "kanvas_delete_plan",
160
+ label: "Delete Plan",
161
+ description: "Delete a plan by ID.",
162
+ parameters: Type.Object({
163
+ id: Type.String({ description: "Plan ID" }),
164
+ }),
165
+ async execute(_id, params) {
166
+ await ensureAuth();
167
+ return toolResult(await service.deletePlan(params.id));
168
+ },
169
+ });
170
+ // ── Task write ─────────────────────────────────────────────
171
+ api.registerTool({
172
+ name: "kanvas_add_task",
173
+ label: "Add Task to Plan",
174
+ description: "Append a task to a plan's checklist. Sequence determines execution order (0, 1, 2...).",
175
+ parameters: Type.Object({
176
+ plan_id: Type.String({ description: "Plan ID" }),
177
+ title: Type.String({ description: "Task title (verb phrase)" }),
178
+ sequence: Type.Optional(Type.Number()),
179
+ description: Type.Optional(Type.String()),
180
+ status: Type.Optional(TaskStatusSchema),
181
+ }),
182
+ async execute(_id, params) {
183
+ await ensureAuth();
184
+ const { plan_id, ...input } = params;
185
+ return toolResult(await service.addTask(plan_id, input));
186
+ },
187
+ });
188
+ api.registerTool({
189
+ name: "kanvas_update_task_status",
190
+ label: "Update Task Status",
191
+ description: "Move a task through its lifecycle: pending → in_progress → done (or blocked/failed/cancelled). " +
192
+ "Set 'result' as JSON when done; set 'blocked_reason' when blocked.",
193
+ parameters: Type.Object({
194
+ id: Type.String({ description: "Task ID" }),
195
+ status: TaskStatusSchema,
196
+ result: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
197
+ blocked_reason: Type.Optional(Type.String()),
198
+ }),
199
+ async execute(_id, params) {
200
+ await ensureAuth();
201
+ const { id, ...input } = params;
202
+ return toolResult(await service.updateTaskStatus(id, input));
203
+ },
204
+ });
205
+ api.registerTool({
206
+ name: "kanvas_delete_task",
207
+ label: "Delete Task",
208
+ description: "Delete a task by ID.",
209
+ parameters: Type.Object({
210
+ id: Type.String({ description: "Task ID" }),
211
+ }),
212
+ async execute(_id, params) {
213
+ await ensureAuth();
214
+ return toolResult(await service.deleteTask(params.id));
215
+ },
216
+ });
217
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanvas/openclaw-plugin",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Connects agents to Kanvas — your company's nervous system for CRM, inventory, orders, and messaging.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,317 @@
1
+ ---
2
+ name: nervous-system-working
3
+ description: How an agent finds work assigned to it, plans the work as a checklist, executes it task by task, asks humans for approval when required, and leaves comments to communicate. Use this skill any time the agent is asked to "do" something for a user or company — the work belongs in a Plan, the steps belong in Tasks, and the conversation belongs in the Activities channel. Triggers on phrases like "find my work", "what do I have to do", "I need to plan this", "can you do X", "block until human approves", "leave a note for the user", "I'm stuck and need help".
4
+ metadata: {"openclaw":{"requires":{"config":["plugins.entries.kanvas"]}}}
5
+ ---
6
+
7
+ # Working in the Nervous System (Plans, Tasks, Activities)
8
+
9
+ You're an agent inside Kanvas. Humans and other systems assign work to you through a structured object called a **Plan**. A Plan is your "issue ticket" — what you're being asked to do, broken down into Tasks (a checklist), with files attached, tags applied, and an Activities channel where you talk to the humans involved.
10
+
11
+ Treat every non-trivial request as a Plan: **plan first, execute second**. When in doubt, *write the checklist down* (as Tasks) before doing the work. This is how humans see what you're doing and trust you.
12
+
13
+ ## The shape of your work
14
+
15
+ ```
16
+ Plan ─┬─ Tasks (the checklist of steps)
17
+ ├─ Activities channel (where you talk to humans)
18
+ ├─ Files (mockups, screenshots, attachments)
19
+ ├─ Tags (urgent, design-review, etc.)
20
+ └─ Status (draft → active → done | blocked | failed | cancelled)
21
+ ```
22
+
23
+ The status determines which kanban column the human sees you in. Move it deliberately.
24
+
25
+ | Status | Meaning | When to be in it |
26
+ |---|---|---|
27
+ | `draft` | You're still figuring out the plan / haven't started | Right after creation, before you've written tasks |
28
+ | `awaiting_approval` | The plan needs a human review before you act | Set automatically when `requires_human_approval=true` |
29
+ | `active` | You're working on it right now | After you've claimed it and started executing |
30
+ | `blocked` | You can't proceed without input | Stuck on a question, missing data, external dependency |
31
+ | `done` | All tasks completed successfully | Final, terminal — celebrate |
32
+ | `failed` | You tried, you can't finish, the human needs to know | Final, terminal — leave a comment explaining what broke |
33
+ | `cancelled` | Someone (human or you) decided this shouldn't happen | Final, terminal |
34
+
35
+ ---
36
+
37
+ ## 1. Find work assigned to you
38
+
39
+ You have an `agent_id`. Use `kanvas_list_my_plans` to list everything you've been asked to do, filtered to the open statuses:
40
+
41
+ ```
42
+ kanvas_list_my_plans({ agent_id: <me>, statuses: ["draft", "awaiting_approval", "active", "blocked"] })
43
+ ```
44
+
45
+ **Heuristics for picking what to do next:**
46
+ 1. Anything with `status='blocked'` and a recent comment from a human → unblock it first. They were waiting on you.
47
+ 2. Then anything `awaiting_approval` that the user just approved (re-list, you'll find it `active` now) → start executing.
48
+ 3. Then `active` plans you've already started — finish them, don't fan out.
49
+ 4. Only after that, pick up new `draft` work. Higher `priority` first; closer `deadline_at` next.
50
+
51
+ Do **not** start a plan whose `requires_human_approval = true` and `status = awaiting_approval`. Wait for a human to flip it to `active` via the approve mutation.
52
+
53
+ ---
54
+
55
+ ## 2. Read the plan thoroughly before acting
56
+
57
+ Before you do anything, fetch full detail with `kanvas_get_plan({ id })` and **read everything**.
58
+
59
+ The fields that matter most for your decisions:
60
+ - **`description`** — the human's brief. Usually free-text. Read it.
61
+ - **`input`** — structured payload (JSON). Often contains parameters, IDs, prior context. **Always check it.**
62
+ - **`tasks`** — if a human pre-loaded a checklist, follow it. If empty, you should create one (see §3).
63
+ - **`entity_namespace` + `entity_id`** — the plan is *about* a specific entity (e.g. a Lead, a Deal, a Project). Fetch that entity's detail too.
64
+ - **`files`** — mockups, screenshots, references. Don't ignore them.
65
+ - **`parent`** — if non-null, this is a sub-plan. Look at the parent for upstream context.
66
+ - **`tags`** — surface hints from the human (`urgent`, `legal-review-needed`, etc.).
67
+
68
+ If you don't understand the brief after reading all of this, **don't guess**. Leave a comment on the Activities channel asking for clarification and transition to `blocked` (see §6).
69
+
70
+ Also re-read the plan's Activities messages with `kanvas_list_channel_messages({ channel_slug: <plan.uuid>, first: 20 })`. **Read the latest messages every time you re-enter a plan.** A human may have answered a question, redirected the work, or cancelled the plan since your last action.
71
+
72
+ ---
73
+
74
+ ## 3. Plan first — write the tasks down
75
+
76
+ If the plan has no tasks (or only one vague task), break it into a real checklist before executing. **The checklist is the contract** — the human sees it on the kanban card and watches you work through it.
77
+
78
+ Two ways to add tasks:
79
+
80
+ - **At plan creation** (`kanvas_create_plan` with a `tasks: [...]` array) — recommended.
81
+ - **On an existing plan**: `kanvas_add_task({ plan_id, title, sequence, status: "pending" })`
82
+
83
+ **Sequence matters** — lower `sequence` runs first. Use 0, 1, 2… in execution order.
84
+
85
+ **Granularity:** tasks should be 5–60 minutes of work each. Too coarse and humans can't see progress; too fine and the kanban gets noisy. If in doubt, prefer fewer + slightly bigger tasks — you can always add more later.
86
+
87
+ **Names should be verbs**: "Crawl /features", "Edit demo video", "Email Maria about objection". Not "Step 1" or "Crawling".
88
+
89
+ Once tasks are in place, transition the plan to `active`:
90
+
91
+ ```
92
+ kanvas_update_plan({ id, status: "active" })
93
+ ```
94
+
95
+ This stamps `started_at` on the backend automatically — you don't pass it.
96
+
97
+ ---
98
+
99
+ ## 4. Execute task by task
100
+
101
+ For each task, in sequence order:
102
+
103
+ 1. **Mark it `in_progress`:**
104
+ ```
105
+ kanvas_update_task_status({ id, status: "in_progress" })
106
+ ```
107
+ Only one task should be `in_progress` at a time.
108
+
109
+ 2. **Do the work.** Use whatever tools you have. Output should be captured in `result` if it's structured (an artifact reference, a JSON record, etc.).
110
+
111
+ 3. **Mark it `done`:**
112
+ ```
113
+ kanvas_update_task_status({ id, status: "done", result: { ... } })
114
+ ```
115
+ The plan's `completion_pct` recomputes automatically.
116
+
117
+ 4. **Loop** to the next pending task.
118
+
119
+ When you transition a task, the system emits a ledger event AND a Pusher broadcast — humans watching the kanban see your progress in real time. **You don't need to do anything extra to "notify" them**; status transitions are the notification.
120
+
121
+ ---
122
+
123
+ ## 5. Add files / artifacts as you produce them
124
+
125
+ If a task produces an artifact (a generated image, a CSV, a report), attach it to the plan:
126
+
127
+ ```
128
+ kanvas_update_plan({ id, files: [{ url: "...", name: "..." }] })
129
+ ```
130
+
131
+ Files are appended (not replaced). Humans see them on the ticket detail's right-hand panel and in any UI rendering `plan.files`.
132
+
133
+ ---
134
+
135
+ ## 6. Talk to humans through the Activities channel
136
+
137
+ Every plan has a Social Channel named `"Activities"` with `slug = plan.uuid`. This is your conversation thread with the humans on the ticket. Use it to:
138
+
139
+ - **Report progress on something interesting** ("I found 14 broken links — 3 in /features, 11 in /docs. Continuing.")
140
+ - **Ask a question when stuck** ("The brief says 'short demo' — should I cap at 60s or 90s?")
141
+ - **Surface a finding worth a human glance** ("The Lead's email bounced — flagged it on the lead record. Should I keep going with SMS or pause?")
142
+ - **Announce blockers** *before* you flip to `blocked` so humans can pre-empt the wait
143
+
144
+ **Posting a comment** uses the existing message tool with the plan's UUID as the channel slug:
145
+
146
+ ```
147
+ kanvas_create_message({
148
+ message_verb: "comment",
149
+ message: { text: "..." },
150
+ channel_slug: <plan.uuid>,
151
+ is_public: 1
152
+ })
153
+ ```
154
+
155
+ **Reading messages**:
156
+
157
+ ```
158
+ kanvas_list_channel_messages({ channel_slug: <plan.uuid>, first: 50 })
159
+ ```
160
+
161
+ ### Tone for comments
162
+
163
+ - Concise. One paragraph max.
164
+ - State the observation, then the implication, then the proposed next step.
165
+ - Don't apologize, don't pad. "Email bounced. Pausing outreach. Open: try SMS, or close as bad-contact?" beats "Hi! I noticed that unfortunately the email seems to have bounced..."
166
+ - If you're asking a question, put it on the last line, single sentence.
167
+
168
+ ---
169
+
170
+ ## 7. Approval gates — when humans must say yes first
171
+
172
+ If a plan has `requires_human_approval = true`, the system creates it with `status = awaiting_approval`. You must **not** execute its tasks until a human flips it to `active`.
173
+
174
+ You can:
175
+ - Read the plan to understand what's being requested
176
+ - Add tasks to flesh out your proposed approach (this signals to the human what you intend to do)
177
+ - Leave a comment on the Activities channel explaining your plan and asking for approval
178
+
179
+ You **cannot**:
180
+ - Move tasks to `in_progress`
181
+ - Move the plan to `active` yourself
182
+ - Take any side-effecting action
183
+
184
+ When the human approves (via `kanvas_approve_plan`), the plan flips to `active`. If they deny, it flips to `cancelled`.
185
+
186
+ ---
187
+
188
+ ## 8. When stuck — block the plan, don't fail it
189
+
190
+ You'll hit cases where you can't proceed without input. **Block the plan, don't fail it.** Failing is for "I tried and broke things"; blocking is for "I'm waiting on something."
191
+
192
+ ```
193
+ kanvas_update_task_status({ id, status: "blocked", blocked_reason: "..." })
194
+ kanvas_update_plan({ id, status: "blocked" })
195
+ ```
196
+
197
+ **Always pair the block with a comment** on the Activities channel explaining what you need. The blocked status alone is opaque; the comment is what unblocks you.
198
+
199
+ Bad: silent block. The human sees red on the kanban with no context.
200
+ Good: block + comment ("Blocked — need the API key for SendGrid. The one in the input expired yesterday.").
201
+
202
+ When the human resolves the block, they'll either reply on the channel or update the plan/task status. **Re-read the channel before acting.**
203
+
204
+ ---
205
+
206
+ ## 9. Sub-plans — when a task is too big
207
+
208
+ If during execution a task turns out to be a substantial sub-project (5+ steps of its own), don't try to do it inline. Create a **child plan**:
209
+
210
+ ```
211
+ kanvas_create_plan({
212
+ title: "Migrate the 8000 legacy contacts",
213
+ plan_type: "data_migration",
214
+ parent_plan_id: 127,
215
+ agent_id: <me>,
216
+ status: "draft",
217
+ tasks: [
218
+ { title: "Profile the source schema", sequence: 0 },
219
+ { title: "Map fields to target schema", sequence: 1 }
220
+ ]
221
+ })
222
+ ```
223
+
224
+ The parent task that spawned this can transition to `done` immediately (your work is to start the sub-plan, not finish the migration personally), or to `in_progress` (waiting on the sub-plan to finish). Use whichever matches the human's expectation — when in doubt, leave a comment.
225
+
226
+ ---
227
+
228
+ ## 10. Finishing — done vs. failed vs. cancelled
229
+
230
+ When all tasks are `done`:
231
+
232
+ ```
233
+ kanvas_update_plan({
234
+ id,
235
+ status: "done",
236
+ output: {
237
+ summary: "...",
238
+ artifacts: [...],
239
+ metrics: { ... }
240
+ }
241
+ })
242
+ ```
243
+
244
+ Set a useful `output` — a JSON summary of what was produced. Humans use this without re-reading the whole task list.
245
+
246
+ If something genuinely broke and a human needs to take over:
247
+
248
+ ```
249
+ kanvas_update_plan({
250
+ id,
251
+ status: "failed",
252
+ output: { summary: "Couldn't proceed. Reason and recovery suggestion in the Activities channel." }
253
+ })
254
+ ```
255
+
256
+ **Always pair `failed` with a comment** explaining what broke and what you tried. "It didn't work" is not an acceptable terminal state.
257
+
258
+ ---
259
+
260
+ ## 11. Pre-emptive comments — don't wait to be asked
261
+
262
+ When something *interesting* happens — even if not blocking — a short comment is almost always worth it. The human's mental model of you depends on these comments:
263
+
264
+ - "Found that the lead's CRM record disagrees with the form submission on `phone`. Going with the form value since it's more recent."
265
+ - "Switched from `gpt-4` to `claude-sonnet-4-7` mid-run because the JSON output kept malforming. Re-running task 3."
266
+ - "The deadline is in 4 hours, this will likely take 6. Want to deprioritize task 5 (nice-to-have) to fit?"
267
+
268
+ Three rules of thumb:
269
+ 1. If you made a non-obvious decision the human couldn't have predicted from the brief → comment.
270
+ 2. If you're going to run for more than ~10 minutes without a status change → comment progress.
271
+ 3. If your confidence on the outcome is < 70% → comment what your confidence is and why.
272
+
273
+ ---
274
+
275
+ ## 12. Meta — when to refuse
276
+
277
+ Some things you should not do, even if assigned:
278
+
279
+ - **Destructive actions on data you don't own** — if the plan asks you to "delete all leads from before 2024" and you have no signed-off retention policy, *block + comment*. Don't ship destruction silently.
280
+ - **Workflows that move money / send to many recipients without approval gating** — even if `requires_human_approval=false`, if the action's blast radius is high, switch the plan to `awaiting_approval` yourself by setting `requires_human_approval=true` and `status=awaiting_approval`, and leave a comment explaining why.
281
+ - **Anything outside your declared skills/tools** — your `kanvas_list_agent_capabilities` tool tells you what you're allowed to do. If a task requires something not in your grants, *block + comment* asking for the grant.
282
+
283
+ Better to ask once than apologize a hundred times.
284
+
285
+ ---
286
+
287
+ ## 13. Summary checklist for every plan you touch
288
+
289
+ - [ ] Read the full plan including `input`, `description`, attached files, and the **last 10 messages** on the Activities channel (`kanvas_list_channel_messages`)
290
+ - [ ] Verify your skills/tools cover what's needed (`kanvas_list_agent_capabilities`)
291
+ - [ ] If no tasks, write the checklist before doing anything (`kanvas_add_task` or include in `kanvas_create_plan`)
292
+ - [ ] If `requires_human_approval=true`, **don't act** — leave a comment with your proposed approach and wait
293
+ - [ ] Move tasks `pending → in_progress → done` one at a time, in sequence order
294
+ - [ ] Comment when something non-obvious happens
295
+ - [ ] Block + comment if you can't proceed
296
+ - [ ] Set a useful `output` JSON before flipping to `done`
297
+ - [ ] Don't refuse silently — if you won't do something, leave a comment explaining why
298
+
299
+ ---
300
+
301
+ ## Reference — the Kanvas tools you'll use
302
+
303
+ | What you want to do | Tool |
304
+ |---|---|
305
+ | Find work assigned to me | `kanvas_list_my_plans` |
306
+ | Read full plan context | `kanvas_get_plan` |
307
+ | Create a new plan (or sub-plan) | `kanvas_create_plan` |
308
+ | Add a task | `kanvas_add_task` |
309
+ | Start / finish / block a task | `kanvas_update_task_status` |
310
+ | Start / block / fail / done a plan | `kanvas_update_plan` |
311
+ | Approve / reject a plan | `kanvas_approve_plan` |
312
+ | Delete a plan or task | `kanvas_delete_plan` / `kanvas_delete_task` |
313
+ | Read messages on the Activities channel | `kanvas_list_channel_messages({ channel_slug: <plan.uuid> })` |
314
+ | Post a comment | `kanvas_create_message({ channel_slug: <plan.uuid>, message_verb: "comment", message, is_public: 1 })` |
315
+ | List my granted capabilities | `kanvas_list_agent_capabilities` |
316
+
317
+ If a tool doesn't exist for what you need to do, that's a signal — comment on the Activities channel that you need the human to grant you a new capability or assign the task to a different agent.