
Automate Whatsapp
- 2.6k installs
- 144 repo stars
- Updated August 2, 2026
- gokapso/agent-skills
automate-whatsapp is a Kapso skill for WhatsApp workflow automation, triggers, graph edits, and function deployment.
About
The automate-whatsapp skill builds WhatsApp automations with Kapso workflows covering CRUD, graph edits, triggers, executions, functions, and MCP tools. Preferred setup uses kapso login, kapso link, kapso pull, kapso build, and kapso push on source-controlled projects exporting a Workflow from @kapso/workflows in workflow.js or workflow.ts. Workflows add inbound_message triggers with phoneNumberId, nodes such as send_text, and edges from START to reply steps before dry-run push. Phone numbers resolve via kapso whatsapp numbers list and resolve commands before trigger creation. API script fallbacks fetch graphs with lock_version, validate JSON, and update with expected-lock-version to avoid conflicts. Graph rules require one start node id start, stable existing node IDs, timestamped new IDs, and decide edge labels matching conditions. Functions use async handler(request, env) returning Response without export defaults. Execution debugging lists runs, inspects context vars and system metadata, and reads execution events. Agent remote sandbox nodes mount GitHub repos under /workspace/repos for beta file-aware agents.
- Preferred path uses kapso link, pull, build, and push on @kapso/workflows sources.
- Resolve WhatsApp phone_number_id via CLI before inbound_message triggers.
- Graph updates require lock_version; retry after re-fetch on conflict errors.
- Functions use async handler returning Response without export or arrow wrappers.
- Execution context splits vars, system, context channel data, and metadata fields.
Automate Whatsapp by the numbers
- 2,557 all-time installs (skills.sh)
- +66 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #165 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
automate-whatsapp capabilities & compatibility
- Capabilities
- local @kapso/workflows source sync with build an · graph fetch, validate, and lock_version safe upd · trigger crud for inbound_message phone number bi · execution listing, context inspection, and event · function create, deploy, and public endpoint con
- Works with
- slack
- Use cases
- orchestration · email
- Pricing
- Freemium
What automate-whatsapp says it does
Use this skill to build and run WhatsApp automations: workflow CRUD, graph edits, triggers, executions
npx skills add https://github.com/gokapso/agent-skills --skill automate-whatsappAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 144 |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 2, 2026 |
| Repository | gokapso/agent-skills ↗ |
How do I automate WhatsApp conversations with Kapso workflows and debug failing executions?
Build and debug Kapso WhatsApp workflow automations with local source sync, triggers, graph edits, executions, and function deploys.
Who is it for?
Teams building Kapso WhatsApp inbound support, notifications, or agent-driven message flows.
Skip if: Skip for non-WhatsApp channels or manual one-off SMS without Kapso workflow infrastructure.
When should I use this skill?
User mentions Kapso workflows, WhatsApp automation, inbound_message triggers, or workflow graph edits.
What you get
A deployed workflow with triggers, validated graph, and inspectable execution context for inbound messages.
- WhatsApp flow configuration
- Agent node setup
- Button reply routing logic
Files
Automate WhatsApp
When to use
Use this skill to build and run WhatsApp automations: workflow CRUD, graph edits, triggers, executions, function management, webhook tools, and MCP tools.
Setup
Preferred path:
- Kapso CLI installed and authenticated (
kapso login) - For workflow and function edits, use source-controlled projects with
kapso link,kapso pull,kapso build, andkapso push - For workflow code, use
@kapso/workflowsand export aWorkflowinstance fromworkflow.jsorworkflow.ts
Fallback path: Env vars:
KAPSO_API_BASE_URL(host only, no/platform/v1)KAPSO_API_KEY
How to
Edit workflows locally
Use this path first when the user is working in, or can create, a local repo.
npm install -g @kapso/cli
npm install --save-dev @kapso/workflows
kapso login
kapso link --project <project-id>
kapso pullEdit workflows/<workflow-slug>/workflow.js or workflow.ts with @kapso/workflows:
import { START, Workflow } from "@kapso/workflows";
const workflow = new Workflow("inbound-support", {
name: "Inbound Support",
status: "draft",
});
workflow.addTrigger({
type: "inbound_message",
phoneNumberId: "<phone-number-id>",
});
workflow.addNode(START, {
position: { x: 100, y: 100 },
});
workflow.addNode("reply", {
type: "send_text",
message: "Thanks for reaching out.",
});
workflow.addEdge(START, "reply");
export default workflow;Build and push:
kapso build
kapso push --dry-run
kapso push workflow <workflow-slug>Use kapso push to push every local function and workflow. See references/local-workflow-source.md for repo layout, source-file behavior, and JSON-only editing.
Discover phone numbers first
Preferred path: 1. Check project state: kapso status 2. List connected numbers: kapso whatsapp numbers list --output json 3. Resolve a display number when needed: kapso whatsapp numbers resolve --phone-number "<display-number>" --output json
Fallback path: 1. List number configs for triggers: node scripts/list-whatsapp-phone-numbers.js
Edit a workflow graph through API scripts
Prefer local source sync for workflow edits. Use these scripts as a fallback for debugging, direct graph inspection, or API-only environments.
1. Fetch graph: node scripts/get-graph.js <workflow_id> (note the lock_version) 2. Edit the JSON (see graph rules below) 3. Validate: node scripts/validate-graph.js --definition-file <path> 4. Update: node scripts/update-graph.js <workflow_id> --expected-lock-version <n> --definition-file <path> 5. Re-fetch to confirm
For small edits, use edit-graph.js with --old-file and --new-file instead.
If you get a lock_version conflict: re-fetch, re-apply changes, retry with new lock_version.
Manage triggers
1. List: node scripts/list-triggers.js <workflow_id> 2. Create: node scripts/create-trigger.js <workflow_id> --trigger-type <type> --phone-number-id <id> 3. Toggle: node scripts/update-trigger.js --trigger-id <id> --active true|false 4. Delete: node scripts/delete-trigger.js --trigger-id <id>
For inbound_message triggers, prefer kapso whatsapp numbers resolve --phone-number "<display-number>" --output json to get the exact phone_number_id. Fall back to node scripts/list-whatsapp-phone-numbers.js when the CLI is unavailable.
Debug executions
1. List: node scripts/list-executions.js <workflow_id> 2. Inspect: node scripts/get-execution.js <execution-id> 3. Get value: node scripts/get-context-value.js <execution-id> --variable-path vars.foo 4. Events: node scripts/list-execution-events.js <execution-id>
Create and deploy a function
1. Write code with handler signature (see function rules below) 2. Create: node scripts/create-function.js --name <name> --code-file <path> [--public-endpoint true] 3. Deploy: node scripts/deploy-function.js --function-id <id> 4. Verify: node scripts/get-function.js --function-id <id>
Use --public-endpoint true when the function should be callable without X-API-Key via the Kapso-hosted invoke URL. This is only supported for Cloudflare functions. New functions default to invoke_response_mode=passthrough, which returns the function body directly on successful invoke. Legacy wrapped functions can be migrated later with update-function.js.
Set up agent node with remote sandbox repositories
Use this when the agent needs a remote ephemeral workspace to inspect or modify repository files during a workflow run.
1. Read references/agent-remote-sandbox.md for the execution model and field rules 2. Find model: node scripts/list-provider-models.js 3. Copy assets/agent-remote-sandbox-github-repo-example.json as a starting point, or edit the agent node under data.config 4. Set sandbox_enabled: true 5. Set sandbox_network_mode to allow_all or allow_list 6. If using allow_list, add extra outbound hosts in sandbox_allowed_outbound_hosts 7. Add GitHub repositories to flow_agent_resources with:
resource_type: "github_repository"repo_urlbranchpat
8. Write the system prompt so it explicitly reads from /workspace/repos/<repo-slug> before making changes 9. Validate and update the graph
Notes:
- Remote sandbox is beta and free during the beta
sandbox_enabledcontrols whether the remote workspace and sandbox tools are available- Repository resources stay configured even if sandbox access is turned off later
- v1 supports GitHub repositories only
- Use a repository root URL, not a GitHub file URL or
tree/...URL - Repositories are mounted into
/workspace/repos/<repo-slug>inside the remote sandbox - Use
references/agent-remote-sandbox.mdandreferences/node-types.mdfor the exact shape
Graph rules
- Exactly one start node with
id=start - Never change existing node IDs
- Use
{node_type}_{timestamp_ms}for new node IDs - Non-decide nodes have 0 or 1 outgoing
nextedge - Decide edge labels must match
conditions[].label - Edge keys are
source/target/label(notfrom/to)
For full schema details, see references/graph-contract.md.
Function rules
async function handler(request, env) {
// Parse input
const body = await request.json();
// Use env.KV and secrets as needed
return new Response(JSON.stringify({ result: "ok" }));
}- Do NOT use
export,export default, or arrow functions - Return a
Responseobject
Execution context
Always use this structure:
vars- user-defined variablessystem- system variablescontext- channel datametadata- request metadata
Scripts
Workflows
| Script | Purpose |
|---|---|
list-workflows.js | List workflows (metadata only) |
get-workflow.js | Get workflow metadata |
create-workflow.js | Create a workflow |
update-workflow-settings.js | Update workflow settings |
Graph
| Script | Purpose |
|---|---|
get-graph.js | Get workflow graph + lock_version |
edit-graph.js | Patch graph via string replacement |
update-graph.js | Replace entire graph |
validate-graph.js | Validate graph structure locally |
Triggers
| Script | Purpose |
|---|---|
list-triggers.js | List triggers for a workflow |
create-trigger.js | Create a trigger |
update-trigger.js | Enable/disable a trigger |
delete-trigger.js | Delete a trigger |
list-whatsapp-phone-numbers.js | List phone numbers for trigger setup |
Executions
| Script | Purpose |
|---|---|
list-executions.js | List executions |
get-execution.js | Get execution details |
get-context-value.js | Read value from execution context |
update-execution-status.js | Force execution state |
resume-execution.js | Resume waiting execution |
list-execution-events.js | List execution events |
Functions
| Script | Purpose |
|---|---|
list-functions.js | List project functions |
get-function.js | Get function details + code |
create-function.js | Create a function, optionally with a public invoke endpoint |
update-function.js | Update function code, public endpoint setting, or migrate a legacy wrapped function to passthrough |
deploy-function.js | Deploy function to runtime |
invoke-function.js | Invoke function with payload |
list-function-invocations.js | List function invocations |
OpenAPI
| Script | Purpose |
|---|---|
openapi-explore.mjs | Explore OpenAPI (search/op/schema/where) |
Install deps (once):
npm iExamples:
node scripts/openapi-explore.mjs --spec workflows search "variables"
node scripts/openapi-explore.mjs --spec workflows op getWorkflowVariablesNotes
- Prefer file paths over inline JSON (
--definition-file,--code-file) - Variable CRUD (
variables-set.js,variables-delete.js) is blocked - Platform API doesn't support it
References
Read before editing:
- references/local-workflow-source.md - CLI source sync, repo layout, and
@kapso/workflows - references/graph-contract.md - Graph schema, computed vs editable fields, lock_version
- references/node-types.md - Node types and config shapes
- references/workflow-overview.md - Execution flow and states
Other references:
- references/execution-context.md - Context structure and variable substitution
- references/triggers.md - Trigger types and setup
- references/agent-remote-sandbox.md - Remote sandbox behavior, repo resources, mounted paths
- references/functions-reference.md - Function management
- references/functions-payloads.md - Payload shapes for functions
Assets
| File | Description |
|---|---|
workflow-linear.json | Minimal linear workflow |
workflow-decision.json | Minimal branching workflow |
workflow-agent-simple.json | Minimal agent workflow |
workflow-customer-support-intake-agent.json | Customer support intake |
workflow-interactive-buttons-decide-function.json | Interactive buttons + decide (function) |
workflow-interactive-buttons-decide-ai.json | Interactive buttons + decide (AI) |
workflow-api-template-wait-agent.json | API trigger + template + agent |
function-decide-route-interactive-buttons.json | Function for button routing |
agent-remote-sandbox-github-repo-example.json | Agent node with remote sandbox + GitHub repo resource |
Related skills
integrate-whatsapp- Onboarding, webhooks, messaging, templates, flowsobserve-whatsapp- Debugging, logs, health checks
<!-- FILEMAP:BEGIN -->
[automate-whatsapp file map]|root: .
|.:{package.json,SKILL.md}
|assets:{agent-remote-sandbox-github-repo-example.json,function-decide-route-interactive-buttons.json,functions-example.json,workflow-agent-simple.json,workflow-api-template-wait-agent.json,workflow-customer-support-intake-agent.json,workflow-decision.json,workflow-interactive-buttons-decide-ai.json,workflow-interactive-buttons-decide-function.json,workflow-linear.json}
|references:{agent-remote-sandbox.md,execution-context.md,function-contracts.md,functions-payloads.md,functions-reference.md,graph-contract.md,local-workflow-source.md,node-types.md,triggers.md,workflow-overview.md,workflow-reference.md}
|scripts:{create-function.js,create-trigger.js,create-workflow.js,delete-trigger.js,deploy-function.js,edit-graph.js,get-context-value.js,get-execution-event.js,get-execution.js,get-function.js,get-graph.js,get-workflow.js,invoke-function.js,list-execution-events.js,list-executions.js,list-function-invocations.js,list-functions.js,list-provider-models.js,list-triggers.js,list-whatsapp-phone-numbers.js,list-workflows.js,openapi-explore.mjs,resume-execution.js,update-execution-status.js,update-function.js,update-graph.js,update-trigger.js,update-workflow-settings.js,validate-graph.js,variables-delete.js,variables-list.js,variables-set.js}
|scripts/lib/functions:{args.js,kapso-api.js}
|scripts/lib/workflows:{args.js,kapso-api.js,result.js}<!-- FILEMAP:END -->
{
"agent_node_config": {
"node_type": "agent",
"config": {
"system_prompt": "Inspect the mounted repository before answering. Read the README and the most relevant files under /workspace/repos/acme-app, summarize the architecture, and then propose the smallest safe change.",
"provider_model_id": "uuid",
"max_iterations": 20,
"max_tokens": 8192,
"temperature": 0.2,
"sandbox_enabled": true,
"sandbox_network_mode": "allow_list",
"sandbox_allowed_outbound_hosts": [
"api.example.com"
],
"flow_agent_resources": [
{
"resource_type": "github_repository",
"repo_url": "https://github.com/acme/acme-app",
"branch": "main",
"pat": "github_pat_replace_me"
}
],
"flow_agent_webhooks": [],
"flow_agent_function_tools": [],
"flow_agent_mcp_servers": []
}
}
}
{
"name": "decide-route-interactive-buttons",
"description": "Decide node helper: route to the next edge based on vars.button_choice from an interactive button reply.",
"code": "async function handler(request, env) {\n const payload = await request.json();\n\n const executionContext = payload && payload.execution_context ? payload.execution_context : {};\n const vars = executionContext && executionContext.vars ? executionContext.vars : {};\n const availableEdges = Array.isArray(payload && payload.available_edges) ? payload.available_edges : [];\n\n const raw = vars.button_choice ?? vars.last_user_input;\n\n function asString(value) {\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return String(value);\n if (value && typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return null;\n }\n }\n return null;\n }\n\n function extractChoice(value) {\n if (!value) return null;\n\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n return trimmed;\n }\n\n if (value && typeof value === 'object') {\n const v = value;\n const direct = v.button_id || v.buttonId || v.list_id || v.listId || v.id || v.choice || v.value;\n if (typeof direct === 'string' && direct.trim().length > 0) return direct.trim();\n\n // Common nested shapes.\n const nested =\n (v.interactive && (v.interactive.button_reply || v.interactive.list_reply)) ||\n v.button_reply ||\n v.list_reply;\n\n if (nested && typeof nested === 'object') {\n const nestedId = nested.id || nested.button_id || nested.list_id;\n if (typeof nestedId === 'string' && nestedId.trim().length > 0) return nestedId.trim();\n }\n }\n\n return null;\n }\n\n const extracted = extractChoice(raw);\n const normalized = extracted ? extracted.toLowerCase().trim() : null;\n\n // Map common variants to canonical edge labels.\n const mapped = normalized === 'sale' ? 'sales' : normalized;\n\n if (mapped && availableEdges.includes(mapped)) {\n return new Response(JSON.stringify({ next_edge: mapped }), {\n headers: { 'Content-Type': 'application/json' }\n });\n }\n\n const fallback = availableEdges[0] || 'next';\n const reason = {\n extracted: extracted,\n raw_preview: asString(raw),\n available_edges: availableEdges\n };\n\n return new Response(JSON.stringify({ next_edge: fallback, vars: { decision_reason: reason } }), {\n headers: { 'Content-Type': 'application/json' }\n });\n}\n"
}
{
"name": "webhook-handler",
"description": "Store webhook payloads",
"code": "async function handler(request, env) {\n const payload = await request.json();\n const key = `webhook:${Date.now()}`;\n await env.KV.put(key, JSON.stringify({ event: payload.event, data: payload.data }));\n return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } });\n}"
}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": {
"node_type": "start",
"config": {}
}
},
{
"id": "agent_1710000100000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "agent",
"config": {
"system_prompt": "You are a helpful assistant. Ask one clarifying question if needed, then call complete_task.",
"provider_model_id": "PROVIDER_MODEL_ID_HERE",
"max_iterations": 10,
"temperature": 0.2
}
}
}
],
"edges": [
{ "source": "start", "target": "agent_1710000100000", "label": "next" }
]
}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": {
"node_type": "start",
"config": {}
}
},
{
"id": "send_template_1710000500000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "send_template",
"config": {
"template_id": "TEMPLATE_ID_HERE",
"parameters": {
"1": "{{vars.customer_name}}",
"2": "{{vars.case_id}}"
}
}
}
},
{
"id": "wait_for_response_1710000504000",
"type": "flow-node",
"position": { "x": 120, "y": 520 },
"data": {
"node_type": "wait_for_response",
"config": {
"save_response_to": "user_reply"
}
}
},
{
"id": "agent_1710000508000",
"type": "flow-node",
"position": { "x": 120, "y": 720 },
"data": {
"node_type": "agent",
"config": {
"system_prompt": "You are following up on a WhatsApp template message triggered by an API call. Use vars.case_id and vars.customer_name for context. The user's reply is in vars.user_reply. Resolve the request or gather the missing details. When complete, summarize and call complete_task.",
"provider_model_id": "PROVIDER_MODEL_ID_HERE",
"max_iterations": 20,
"temperature": 0.2
}
}
}
],
"edges": [
{ "source": "start", "target": "send_template_1710000500000", "label": "next" },
{ "source": "send_template_1710000500000", "target": "wait_for_response_1710000504000", "label": "next" },
{ "source": "wait_for_response_1710000504000", "target": "agent_1710000508000", "label": "next" }
]
}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": {
"node_type": "start",
"config": {}
}
},
{
"id": "send_text_1710000200000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "send_text",
"config": {
"message": "Thanks for reaching out. Please reply with your email and a short description of the issue.",
"delay_seconds": 0
}
}
},
{
"id": "wait_for_response_1710000204000",
"type": "flow-node",
"position": { "x": 120, "y": 520 },
"data": {
"node_type": "wait_for_response",
"config": {
"save_response_to": "support_intake"
}
}
},
{
"id": "agent_1710000208000",
"type": "flow-node",
"position": { "x": 120, "y": 720 },
"data": {
"node_type": "agent",
"config": {
"system_prompt": "You are a customer support agent. The user's intake message is in vars.support_intake. Extract the email and issue summary. Ask concise follow-up questions if critical details are missing. When you have enough info, summarize the issue and call complete_task.",
"provider_model_id": "PROVIDER_MODEL_ID_HERE",
"max_iterations": 20,
"temperature": 0.2
}
}
}
],
"edges": [
{ "source": "start", "target": "send_text_1710000200000", "label": "next" },
{ "source": "send_text_1710000200000", "target": "wait_for_response_1710000204000", "label": "next" },
{ "source": "wait_for_response_1710000204000", "target": "agent_1710000208000", "label": "next" }
]
}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": {
"node_type": "start",
"config": {}
}
},
{
"id": "send_text_1710000010000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "send_text",
"config": {
"message": "Do you want to continue? Reply yes or no.",
"delay_seconds": 0
}
}
},
{
"id": "wait_for_response_1710000014000",
"type": "flow-node",
"position": { "x": 120, "y": 520 },
"data": {
"node_type": "wait_for_response",
"config": {
"save_response_to": "user_reply"
}
}
},
{
"id": "decide_1710000018000",
"type": "flow-node",
"position": { "x": 120, "y": 720 },
"data": {
"node_type": "decide",
"config": {
"decision_type": "function",
"function_id": "FUNCTION_ID_HERE",
"conditions": [
{ "label": "yes", "description": "User agreed" },
{ "label": "no", "description": "User declined" }
]
}
}
},
{
"id": "send_text_1710000022000",
"type": "flow-node",
"position": { "x": 0, "y": 920 },
"data": {
"node_type": "send_text",
"config": {
"message": "Great, let's continue!",
"delay_seconds": 0
}
}
},
{
"id": "send_text_1710000026000",
"type": "flow-node",
"position": { "x": 240, "y": 920 },
"data": {
"node_type": "send_text",
"config": {
"message": "No problem. Ending the workflow.",
"delay_seconds": 0
}
}
}
],
"edges": [
{ "source": "start", "target": "send_text_1710000010000", "label": "next" },
{ "source": "send_text_1710000010000", "target": "wait_for_response_1710000014000", "label": "next" },
{ "source": "wait_for_response_1710000014000", "target": "decide_1710000018000", "label": "next" },
{ "source": "decide_1710000018000", "target": "send_text_1710000022000", "label": "yes" },
{ "source": "decide_1710000018000", "target": "send_text_1710000026000", "label": "no" }
]
}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": {
"node_type": "start",
"config": {}
}
},
{
"id": "send_interactive_1710000400000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "send_interactive",
"config": {
"interactive_type": "button",
"body_text": "How can we help you today?",
"buttons": [
{ "id": "sales", "title": "Sales" },
{ "id": "support", "title": "Support" }
]
}
}
},
{
"id": "wait_for_response_1710000404000",
"type": "flow-node",
"position": { "x": 120, "y": 520 },
"data": {
"node_type": "wait_for_response",
"config": {
"save_response_to": "button_choice"
}
}
},
{
"id": "decide_1710000408000",
"type": "flow-node",
"position": { "x": 120, "y": 720 },
"data": {
"node_type": "decide",
"config": {
"decision_type": "ai",
"provider_model_id": "PROVIDER_MODEL_ID_HERE",
"conditions": [
{ "label": "sales", "description": "User selected Sales (button id: sales)" },
{ "label": "support", "description": "User selected Support (button id: support)" }
],
"llm_temperature": 0.0
}
}
},
{
"id": "send_text_1710000412000",
"type": "flow-node",
"position": { "x": 0, "y": 920 },
"data": {
"node_type": "send_text",
"config": {
"message": "Got it - connecting you to Sales.",
"delay_seconds": 0
}
}
},
{
"id": "send_text_1710000416000",
"type": "flow-node",
"position": { "x": 240, "y": 920 },
"data": {
"node_type": "send_text",
"config": {
"message": "Got it - connecting you to Support.",
"delay_seconds": 0
}
}
}
],
"edges": [
{ "source": "start", "target": "send_interactive_1710000400000", "label": "next" },
{ "source": "send_interactive_1710000400000", "target": "wait_for_response_1710000404000", "label": "next" },
{ "source": "wait_for_response_1710000404000", "target": "decide_1710000408000", "label": "next" },
{ "source": "decide_1710000408000", "target": "send_text_1710000412000", "label": "sales" },
{ "source": "decide_1710000408000", "target": "send_text_1710000416000", "label": "support" }
]
}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": {
"node_type": "start",
"config": {}
}
},
{
"id": "send_interactive_1710000300000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "send_interactive",
"config": {
"interactive_type": "button",
"body_text": "How can we help you today?",
"buttons": [
{ "id": "sales", "title": "Sales" },
{ "id": "support", "title": "Support" }
]
}
}
},
{
"id": "wait_for_response_1710000304000",
"type": "flow-node",
"position": { "x": 120, "y": 520 },
"data": {
"node_type": "wait_for_response",
"config": {
"save_response_to": "button_choice"
}
}
},
{
"id": "decide_1710000308000",
"type": "flow-node",
"position": { "x": 120, "y": 720 },
"data": {
"node_type": "decide",
"config": {
"decision_type": "function",
"function_id": "FUNCTION_ID_HERE",
"conditions": [
{ "label": "sales", "description": "User selected Sales" },
{ "label": "support", "description": "User selected Support" }
]
}
}
},
{
"id": "send_text_1710000312000",
"type": "flow-node",
"position": { "x": 0, "y": 920 },
"data": {
"node_type": "send_text",
"config": {
"message": "Got it - connecting you to Sales.",
"delay_seconds": 0
}
}
},
{
"id": "send_text_1710000316000",
"type": "flow-node",
"position": { "x": 240, "y": 920 },
"data": {
"node_type": "send_text",
"config": {
"message": "Got it - connecting you to Support.",
"delay_seconds": 0
}
}
}
],
"edges": [
{ "source": "start", "target": "send_interactive_1710000300000", "label": "next" },
{ "source": "send_interactive_1710000300000", "target": "wait_for_response_1710000304000", "label": "next" },
{ "source": "wait_for_response_1710000304000", "target": "decide_1710000308000", "label": "next" },
{ "source": "decide_1710000308000", "target": "send_text_1710000312000", "label": "sales" },
{ "source": "decide_1710000308000", "target": "send_text_1710000316000", "label": "support" }
]
}
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": {
"node_type": "start",
"config": {}
}
},
{
"id": "send_text_1710000000000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "send_text",
"config": {
"message": "Hello! What's your name?",
"delay_seconds": 0
}
}
},
{
"id": "wait_for_response_1710000005000",
"type": "flow-node",
"position": { "x": 120, "y": 520 },
"data": {
"node_type": "wait_for_response",
"config": {
"save_response_to": "user_name"
}
}
},
{
"id": "send_text_1710000009000",
"type": "flow-node",
"position": { "x": 120, "y": 720 },
"data": {
"node_type": "send_text",
"config": {
"message": "Nice to meet you, {{vars.user_name}}!",
"delay_seconds": 0
}
}
}
],
"edges": [
{ "source": "start", "target": "send_text_1710000000000", "label": "next" },
{ "source": "send_text_1710000000000", "target": "wait_for_response_1710000005000", "label": "next" },
{ "source": "wait_for_response_1710000005000", "target": "send_text_1710000009000", "label": "next" }
]
}
{
"private": true,
"type": "module",
"dependencies": {
"yaml": "^2.6.0"
},
"scripts": {
"openapi": "node scripts/openapi-explore.mjs"
}
}
Agent Remote Sandbox
Use the remote sandbox when an agent node needs a disposable workspace to inspect or modify repository files during execution.
What sandbox access changes
When sandbox_enabled is true, the agent gets a remote ephemeral workspace plus sandbox file tools:
bashreadlist_dirwriteedit
Repository resources are separate from tool definitions. They are mounted into the sandbox only when sandbox access is enabled.
If sandbox access is later turned off, the repository resources stay saved in the node config. They are simply not mounted until sandbox access is enabled again.
Remote sandbox is in beta. Sandbox usage is free during the beta. Pricing may change later.
GitHub repository resources
v1 supports GitHub repositories only.
Each repository entry in flow_agent_resources should include:
{
"resource_type": "github_repository",
"repo_url": "https://github.com/org/repo",
"branch": "main",
"pat": "github_pat_replace_me"
}Rules:
- Use a repository root URL only
- Valid examples:
https://github.com/org/repo,https://github.com/org/repo.git,git@github.com:org/repo.git - Do not use GitHub file URLs, subdirectory URLs, or
tree/...URLs - Each repository needs a GitHub Personal Access Token (PAT)
- Saved responses do not return the PAT; they only return metadata like
has_pat: true
Mounted paths inside the sandbox
Configured repositories are cloned into:
/workspace/repos/<repo-slug>Write the agent prompt so it refers to those mounted paths explicitly. For example:
- inspect
/workspace/repos/acme-appbefore answering - read the README and relevant service files before proposing changes
- make changes only inside the mounted repository unless the workflow explicitly needs something else
Sandbox network policy
Use sandbox_network_mode to control outbound access from the remote sandbox:
allow_all: permit all outbound hostsallow_list: permit only allow-listed hosts
When GitHub repositories are attached, Kapso automatically adds the GitHub hosts needed for cloning and repository access. Add entries to sandbox_allowed_outbound_hosts only for extra services your workflow needs, such as internal APIs or external documentation hosts.
Recommended setup flow
1. Pick a model with node scripts/list-provider-models.js 2. Start from assets/agent-remote-sandbox-github-repo-example.json 3. Enable sandbox_enabled 4. Add one or more GitHub repository resources 5. Choose sandbox_network_mode 6. If using allow_list, add only the extra hosts the agent truly needs 7. Validate with node scripts/validate-graph.js --definition-file <path> 8. Update the workflow graph
Execution Context and Variables
Execution context shape:
{
"vars": { "user_name": "Alice" },
"system": { "flow_id": "uuid", "flow_name": "...", "trigger_type": "inbound_message" },
"context": { "channel": "whatsapp", "phone_number": "+1234567890", "contact": { "wa_id": "...", "profile_name": "..." } },
"metadata": { "request": { "ip": "...", "timestamp": "..." } }
}Always use this structure:
vars: user-defined variablessystem: system variables (flow_id, trigger_type, etc)context: channel info (phone number, contact)metadata: request metadata
Variable syntax
Environment variables (secrets):
${ENV:VARIABLE_NAME}
Runtime variables:
{{vars.my_variable}}{{system.flow_name}}{{context.phone_number}}
Substitution order: 1. Environment variables 2. Runtime variables
Important WhatsApp keys:
{{system.whatsapp_config.phone_number_id}}(Meta phone_number_id){{system.trigger_whatsapp_config_id}}(Kapso WhatsApp config id){{context.phone_number}}(recipient phone)
Never guess variable paths. Use:
scripts/variables-list.js <workflow-id>scripts/get-execution.js <execution-id>scripts/get-context-value.js <execution-id> --variable-path <path>
Workflow Function Contracts
Use this when configuring function or decide nodes.
Code rules (must follow)
- Code MUST start with:
async function handler(request, env) { - Do NOT use
export,export default, or arrow functions. - Output only JavaScript source code (no markdown fences).
Payload (function/decide nodes)
{
"execution_context": { "vars": {}, "system": {}, "context": {}, "metadata": {} },
"available_edges": ["edge_a", "edge_b"],
"flow_events": [{ "event_type": "...", "payload": {} }]
}Function node response
Return variables to merge:
{ "vars": { "processed": true } }Decide node response
Return next_edge matching an outgoing edge label:
{ "next_edge": "qualified", "vars": { "decision_reason": "qualified" } }Always fall back to the first available edge if unsure.
Agent Function Tool
Agent node function tools receive:
{
"input": { "any": "shape" },
"execution_context": { "vars": {}, "system": {}, "context": {}, "metadata": {} },
"flow_events": [],
"flow_info": { "id": "uuid", "name": "...", "step_id": "uuid" },
"whatsapp_context": { "conversation": {}, "messages": [] }
}For full runtime contract details, load:
functions-reference.mdfunctions-payloads.md
Payload Shapes (Quick Reference)
Workflow function node (request)
Kapso sends:
{
"execution_context": {
"vars": { "user_name": "John", "score": 42 },
"system": { "flow_id": "uuid", "flow_name": "...", "trigger_type": "inbound_message" },
"context": { "channel": "whatsapp", "phone_number": "+1234567890", "contact": { "wa_id": "...", "profile_name": "..." } },
"metadata": { "request": { "ip": "...", "timestamp": "..." } }
},
"available_edges": ["edge_a", "edge_b"],
"flow_events": [{ "event_type": "...", "payload": { "...": "..." } }]
}Function node response
Return vars to merge:
{ "vars": { "processed": true, "result": "ok" } }Workflow decide node (request)
Same request shape as function nodes.
Decide node response
Return next_edge that matches an outgoing edge label:
{ "next_edge": "qualified", "vars": { "decision_reason": "qualified" } }Always fall back to the first available edge if unsure.
Agent function tool (request)
{
"input": { "any": "shape" },
"execution_context": { "vars": {}, "system": {}, "context": {}, "metadata": {} },
"flow_events": [{ "event_type": "...", "payload": { "...": "..." } }],
"flow_info": { "id": "uuid", "name": "...", "step_id": "uuid" },
"whatsapp_context": { "conversation": { "...": "..." }, "messages": [] }
}WhatsApp Flow data endpoint (request)
Kapso forwards:
{
"source": "whatsapp_flow",
"flow": { "id": "<uuid>", "meta_flow_id": "<meta_id>" },
"data_exchange": {
"action": "INIT" | "data_exchange" | "BACK",
"screen": "CURRENT_SCREEN_ID",
"data": { "...": "..." },
"flow_token": "opaque-token"
},
"signature_valid": true,
"received_at": "2024-01-01T00:00:00Z"
}WhatsApp Flow response
{
"version": "3.0",
"screen": "NEXT_SCREEN_ID",
"data": {}
}Code rules
- Code MUST start with:
async function handler(request, env) { - Do NOT use
export,export default, or arrow functions. - Output only JavaScript source code (no markdown fences).
Function Runtime Contract
Handler signature
Functions must start with:
async function handler(request, env) {Do not use export or arrow functions. Return a Response object.
Runtime APIs
request: Fetch API Request; useawait request.json()for JSON.env.KV: KV storage with.get(key),.put(key, value),.delete(key).env.SECRET_NAME: Secrets configured in the function settings.
Typical workflow
1. Create function with code that follows the contract. 2. Deploy the function (required before use). 3. Read endpoint_url from the function record after deploy. 4. If the function should accept anonymous callers, set public_endpoint: true when creating or updating it. This is only supported for Cloudflare functions. 5. New functions default to invoke_response_mode: "passthrough", which returns the function response body directly on successful invoke. Legacy wrapped functions can later be updated to passthrough once callers are ready.
Platform API payload envelope
When calling the Platform API directly (not via scripts), wrap attributes under function:
{
"function": {
"name": "...",
"description": "...",
"code": "...",
"invoke_response_mode": "passthrough",
"public_endpoint": false
}
}Notes:
endpoint_urlfor deployed Cloudflare functions ishttps://api.kapso.ai/platform/v1/functions/{function_id}/invoke- Private functions require
X-API-Key - Public Cloudflare functions (
public_endpoint=true) can be invoked without an API key invoke_response_mode=passthroughforwards the successful function response body directlyinvoke_response_mode=wrappedis a legacy mode for older wrapped functions
Workflow node payload (Function / Decide)
When a workflow function node runs, Kapso sends:
{
"execution_context": {
"vars": { "user_name": "John", "score": 42 },
"system": { "flow_id": "uuid", "flow_name": "...", "trigger_type": "inbound_message" },
"context": { "channel": "whatsapp", "phone_number": "+1234567890", "contact": { "wa_id": "...", "profile_name": "..." } },
"metadata": { "request": { "ip": "...", "timestamp": "..." } }
},
"available_edges": ["edge_a", "edge_b"],
"flow_events": [{ "event_type": "...", "payload": { "..." : "..." } }]
}Execution context structure is always:
vars: user-defined variablessystem: system variables (flow_id, trigger_type, etc)context: channel data (phone number, contact)metadata: request metadata
Function node response
Return vars to update context:
{
"vars": { "processed": true, "result": "ok" }
}Decide node response
Return next_edge that matches an outgoing edge label:
{
"next_edge": "qualified",
"vars": { "decision_reason": "qualified" }
}Always fall back to the first available edge when unsure.
Agent function tool payload
If an agent node uses a Function Tool, Kapso calls the function with:
{
"input": { "any": "shape" },
"execution_context": { "vars": {}, "system": {}, "context": {}, "metadata": {} },
"flow_events": [{ "event_type": "...", "payload": { "...": "..." } }],
"flow_info": { "id": "uuid", "name": "...", "step_id": "uuid" },
"whatsapp_context": { "conversation": { "...": "..." }, "messages": [] }
}The tool expects a standard JSON response; you may return vars as above.
WhatsApp Flow data endpoint payload
For WhatsApp Flow data endpoints, Kapso forwards:
{
"source": "whatsapp_flow",
"flow": { "id": "<uuid>", "meta_flow_id": "<meta_id>" },
"data_exchange": {
"action": "INIT" | "data_exchange" | "BACK",
"screen": "CURRENT_SCREEN_ID",
"data": { "...": "..." },
"flow_token": "opaque-token"
},
"signature_valid": true,
"received_at": "2024-01-01T00:00:00Z"
}Respond with:
{
"version": "3.0",
"screen": "NEXT_SCREEN_ID",
"data": {}
}For full Flow JSON details and gotchas, load:
../integrate-whatsapp/references/whatsapp-flows-spec.md
Best practices
- Guard access:
const vars = body.execution_context?.vars || {}; - Do not mutate
execution_contextin place; return newvars. - Use try/catch for external API calls.
- Keep responses under the Meta timeout (10-15s).
Workflow Graph Contract (Workflows / Definition)
This document is the source of truth for editing Kapso workflow graphs over the Platform API.
Endpoints and envelopes
- Fetch graph (definition):
GET /platform/v1/workflows/:id/definition - Returns a workflow record that includes
definition(nodes + edges). - Fetch metadata:
GET /platform/v1/workflows/:id - Returns workflow metadata (including
lock_version) but does NOT includedefinition. - Update graph/settings:
PATCH /platform/v1/workflows/:id - Send
workflow: { ... }(what the scripts use).flow: { ... }is accepted as an alias. - To update the graph, send
workflow: { definition: <definition> }.
Two graph shapes: returned vs editable
Shape returned by get-graph (ReactFlow-style)
GET /workflows/:id/definition returns a ReactFlow-style definition that includes extra/computed fields:
{
"nodes": [
{
"id": "start",
"type": "flow-node",
"position": { "x": 120, "y": 120 },
"data": { "node_type": "start", "config": {}, "display_name": "Start" }
}
],
"edges": [
{
"id": "uuid",
"source": "start",
"target": "send_text_1710000000000",
"label": "next",
"type": "default",
"flow_condition_id": null
}
]
}Minimal editable shape accepted by the API
For PATCH /workflows/:id, the minimal shape you should edit and send is:
{
"nodes": [
{
"id": "start",
"position": { "x": 120, "y": 120 },
"data": { "node_type": "start", "config": {} }
},
{
"id": "send_text_1710000000000",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "send_text",
"config": { "message": "Hello!", "delay_seconds": 0 }
}
}
],
"edges": [
{ "source": "start", "target": "send_text_1710000000000", "label": "next" }
]
}The API ignores/strips extra fields like node.type, data.display_name, edge.id, and edge.type. You can keep them unchanged when roundtripping, but do not rely on editing them.
Nodes
Required:
node.id(string)node.position.x,node.position.y(numbers)node.data.node_type(string)node.data.config(object; per node_type)
Rules:
- Exactly one start node with
id = "start"anddata.node_type = "start". - Never change existing node IDs.
- For new nodes: use
{node_type}_{timestamp_ms}for theid.
Footgun (important):
- If
data.node_typeis missing, the backend defaults it to"start". - If you create a node with an unknown
data.node_type, the backend will create it as a start-like step. - Always validate before updating:
node scripts/validate-graph.js --definition-file <path>and treat warnings as blockers.
Edges
Required:
edge.source(existing node id)edge.target(existing node id)edge.label(string)
Rules:
- Non-decide nodes: 0 or 1 outgoing edge; if present, its
labelmust be"next". - Decide nodes: one outgoing edge per condition; each edge
labelmust matchconfig.conditions[].label.
Optional:
edge.flow_condition_id(only meaningful for decide edges). If present, it must refer to a condition on that decide node.
Decide node conditions (ids)
For decide nodes, config.conditions[] controls valid outgoing edge labels.
- When creating NEW conditions, do not include an
idfield (the backend generates it). - When editing an EXISTING decide node fetched from the API, you may see
conditions[].idandedges[].flow_condition_idin the returned graph. Keep them unchanged unless you have a specific reason to remove/rebuild the decide node.
Computed vs editable fields
Do edit:
node.positionnode.data.node_typenode.data.configedge.source,edge.target,edge.label
Do NOT treat as editable (computed/ignored/unstable):
node.data.display_nameedge.id,edge.type- any
*_namefields (model names, function names, etc.)
Terminal nodes and agent nodes
- Terminal nodes (leaf nodes) are allowed. A node with no outgoing edge ends the workflow after that step completes.
- Agent nodes can be terminal or can continue via a
"next"edge, depending on what you want: - Terminal agent: the agent handles the conversation and finishes via its tools (ex: complete_task or handoff_to_human).
- Continuing agent: add a
"next"edge if you want deterministic post-agent steps (ex: send_text summary).
lock_version conflicts (exact retry pattern)
The Platform API does not currently enforce lock_version for definition updates, so the scripts do a precheck.
Use this pattern: 1. Fetch graph and lock_version: node scripts/get-graph.js <workflow_id> 2. Apply change with lock precheck:
- Small surgical edit:
node scripts/edit-graph.js <workflow_id> --expected-lock-version <n> ... - Full update:
node scripts/update-graph.js <workflow_id> --expected-lock-version <n> --definition-file <path> update-graph.jscan extractdefinitionfrom wrapper JSON, but prefer sending just thedefinitionobject.
3. If you get a conflict error:
- Re-fetch the latest graph to get the new lock_version.
- Re-apply your change on top of the latest definition.
- Retry with the new expected lock version.
Local Workflow Source
Use the Kapso CLI source-sync workflow when the user wants workflows and functions in a local repo.
Setup
npm install -g @kapso/cli
npm install --save-dev @kapso/workflows
kapso login
kapso link --project <project-id>
kapso pullkapso link binds the current directory to one Kapso project. kapso pull writes the local source tree.
kapso.yaml
.kapso/project.json
.kapso/remote-map.json
functions/<function-slug>/function.yaml
functions/<function-slug>/index.js
workflows/<workflow-slug>/workflow.yaml
workflows/<workflow-slug>/definition.json
workflows/<workflow-slug>/workflow.jsCommit kapso.yaml, .kapso/project.json, .kapso/remote-map.json, functions/, and workflows/ when the repo is shared. The remote map stores the last pulled remote state for stale-update and dirty-file checks.
Workflow code
kapso pull creates workflow.js next to definition.json when no workflow source file exists. Edit workflow.js, or create workflow.ts, when code should be the source of truth.
import { START, Workflow } from "@kapso/workflows";
const workflow = new Workflow("support-router", {
name: "Support Router",
status: "draft",
});
workflow.addTrigger({
type: "inbound_message",
phoneNumberId: "<phone-number-id>",
});
workflow.addNode(START, {
position: { x: 100, y: 100 },
});
workflow.addNode("reply", {
type: "send_text",
message: "Thanks for reaching out.",
});
workflow.addEdge(START, "reply");
export default workflow;Use rawConfig as an escape hatch for fields not yet covered by typed helpers. Use slugs for local references when possible: functionSlug for functions and workflowSlug for called workflows.
Build and push
kapso build
kapso push --dry-run
kapso push workflow <workflow-slug>Use kapso push to push every changed local function and workflow. Use kapso push function <function-slug> for one function.
If a workflow source file exists and changed, kapso push compiles it before uploading. If no workflow code file exists, the CLI uses workflow.yaml and definition.json directly, so JSON-only editing still works.
Pull behavior
kapso pullpreserves hand-authoredworkflow.jsandworkflow.tsfiles.kapso pullupdates remote-ownedworkflow.yamlanddefinition.json.kapso pullrefuses to overwrite dirty remote-owned files.kapso pull --diffshows blocked incoming diffs.kapso pull --overwritereplaces local remote-owned files with remote versions.
When code is the source of truth, committing generated workflow.yaml and definition.json is a repo decision. Commit them when reviewable generated diffs are useful; ignore them when the team wants only authored workflow code in git.
When to use API scripts instead
Use scripts/get-graph.js, scripts/update-graph.js, and related Platform API scripts only when:
- the user cannot use the local CLI source-sync repo,
- you need direct graph inspection for debugging,
- a task is not yet supported by
kapso pull/build/push, - or you need one-off API operations such as execution inspection.
Workflow Node Types
Supported node_type values
These are the data.node_type values supported by the Platform API and validated by scripts/validate-graph.js:
startsend_textsend_templatesend_interactivewait_for_responseset_variabledecidecallwebhookfunctionagenthandoff
Messaging nodes are send_text, send_template, and send_interactive.
Node structure:
{
"id": "send_text_1710000000000",
"type": "flow-node",
"position": { "x": 300, "y": 100 },
"data": {
"node_type": "send_text",
"config": {},
"display_name": "Send Text"
}
}Notes:
display_nameis computed by the backend and is not reliably editable.- New node IDs should use
{node_type}_{timestamp_ms}. - Nodes connect from bottom to top; organize the graph vertically (top-down) rather than left-to-right.
start
{ "node_type": "start", "config": {} }Exactly one start node per workflow, id must be start.
send_text
{ "node_type": "send_text", "config": { "message": "Hello {{vars.name}}!", "delay_seconds": 0 } }Optional: whatsapp_config_id, to_phone_number.
send_template
{
"node_type": "send_template",
"config": {
"template_id": "uuid",
"parameters": { "1": "{{vars.name}}" }
}
}Optional: whatsapp_config_id, to_phone_number. Use the integrate-whatsapp skill to find template IDs and parameter formats.
send_interactive (buttons)
{
"node_type": "send_interactive",
"config": {
"interactive_type": "button",
"body_text": "Choose an option:",
"buttons": [
{ "id": "yes", "title": "Yes" },
{ "id": "no", "title": "No" }
]
}
}Max 3 buttons; id + title required (title max 20 chars).
send_interactive (list)
{
"node_type": "send_interactive",
"config": {
"interactive_type": "list",
"body_text": "Select:",
"list_button_text": "View options",
"list_sections": [
{ "title": "Section", "rows": [{ "id": "opt1", "title": "Option 1" }] }
]
}
}send_interactive (cta_url)
{
"node_type": "send_interactive",
"config": {
"interactive_type": "cta_url",
"body_text": "Visit our site",
"cta_display_text": "Open",
"cta_url": "https://example.com"
}
}send_interactive (flow)
{
"node_type": "send_interactive",
"config": {
"interactive_type": "flow",
"body_text": "Open calendar",
"flow_id": "<META_FLOW_ID>",
"flow_cta": "Open",
"flow_action": "navigate",
"flow_action_payload": { "screen": "FIRST_SCREEN" }
}
}Notes:
flow_idis the Meta Flow ID (string).flow_action_payload.screenmust be a valid first screen.
wait_for_response
{ "node_type": "wait_for_response", "config": { "save_response_to": "user_reply" } }Saves the next message into vars.user_reply.
set_variable
{
"node_type": "set_variable",
"config": {
"variable_name": "customer_name",
"variable_value": "{{vars.user_reply}}",
"value_type": "string"
}
}Notes:
- Use
value_type: "string"unless you have a specific reason to store a different type.
decide (AI routing)
{
"node_type": "decide",
"config": {
"decision_type": "ai",
"provider_model_id": "uuid",
"conditions": [
{ "label": "interested", "description": "User shows interest" },
{ "label": "not_interested", "description": "User declines" }
]
}
}decide (function routing)
{
"node_type": "decide",
"config": {
"decision_type": "function",
"function_id": "uuid",
"conditions": [
{ "label": "yes", "description": "Approved" },
{ "label": "no", "description": "Rejected" }
]
}
}Rules:
- Outgoing edge labels must match
conditions[].label. - When creating new conditions, do not include an
idfield.
call (subworkflow)
{ "node_type": "call", "config": { "workflow_id": "uuid", "save_error_to": "subflow_error" } }webhook
{
"node_type": "webhook",
"config": {
"url": "https://api.example.com/endpoint",
"method": "POST",
"headers": { "Authorization": "Bearer {{vars.token}}" },
"body_template": { "phone": "{{context.phone_number}}" },
"save_response_to": "api_result"
}
}headers and body_template must be valid JSON objects.
function
{ "node_type": "function", "config": { "function_id": "uuid", "save_response_to": "fn_result" } }Use automate-whatsapp function scripts to find function IDs and update code.
agent
{
"node_type": "agent",
"config": {
"system_prompt": "You are a helpful assistant...",
"provider_model_id": "uuid",
"max_iterations": 10,
"temperature": 0.7,
"message_delivery_mode": "auto_send_assistant_text"
}
}Notes:
provider_model_idis required. Usescripts/list-provider-models.jsto find it.- Agent tool arrays live inside
data.config(not at thedataroot). message_delivery_modecontrols user-visible text:auto_send_assistant_textsends normal assistant text automatically.tool_onlykeeps normal assistant text internal; the agent must callsend_notification_to_userfor every user-visible message.- With
tool_only, includesend_notification_to_userandenter_waitinginenabled_default_tools. Useenter_waitingafter questions that need a reply.
<!-- TODO: Add a full tool-only agent workflow asset after the API behavior is generally available. -->
Default tools (toggle on/off only):
- complete_task (required)
- handoff_to_human (required)
- enter_waiting
- send_notification_to_user
- send_media
- get_execution_metadata
- get_whatsapp_context
- contact_conversations
- get_current_datetime
- save_variable
- get_variable
- ask_about_file
Use contact_conversations when an agent should list and read older WhatsApp conversations for the same current contact. Call it with action: "list" first, then call action: "read" with a returned conversation_id.
Custom tools:
flow_agent_webhooks[](webhook tools)flow_agent_mcp_servers[](MCP tools)
Agent tools: exact placement and structure
All tool arrays go under data.config of the agent node:
{
"id": "agent_1730000000000",
"type": "flow-node",
"position": { "x": 120, "y": 320 },
"data": {
"node_type": "agent",
"config": {
"system_prompt": "You can schedule appointments and use calendar tools.",
"provider_model_id": "uuid",
"max_iterations": 10,
"temperature": 0.7,
"message_delivery_mode": "tool_only",
"enabled_default_tools": [
"complete_task",
"handoff_to_human",
"enter_waiting",
"send_notification_to_user",
"get_whatsapp_context"
],
"flow_agent_webhooks": [],
"flow_agent_mcp_servers": []
}
}
}Webhook tools (custom HTTP tools)
Use when the agent needs to call arbitrary HTTP endpoints:
{
"flow_agent_webhooks": [
{
"name": "lookup_customer",
"description": "Fetch customer data from internal API",
"url": "https://api.example.com/customers/lookup",
"http_method": "POST",
"headers": {
"Authorization": "Bearer {{vars.api_token}}",
"Content-Type": "application/json"
},
"body": {
"phone_number": "{{context.phone_number}}"
},
"body_schema": {
"type": "object",
"properties": {
"phone_number": { "type": "string" }
},
"required": ["phone_number"]
},
"jmespath_query": null
}
]
}Rules:
body_schemamust be valid JSON Schema.headersandbodymust be JSON objects (not strings).- Tool inputs are defined by
body_schemaand are sent as the webhook JSON body.
MCP server tools
Use to attach MCP servers the agent can call:
{
"flow_agent_mcp_servers": [
{
"name": "files",
"description": "Local file access",
"url": "https://mcp.example.com",
"headers": {
"Authorization": "Bearer {{vars.mcp_token}}"
}
}
]
}Rules:
urlis required.headersmust be a JSON object.- MCP tool inputs are defined by the MCP server's tool schemas (not configured here).
handoff
{ "node_type": "handoff", "config": {} }Ends execution and flags for human takeover.
AI Fields (dynamic content)
Set a field to {"$ai":{}} and add ai_field_config:
{
"message": { "$ai": {} },
"provider_model_id": "uuid",
"ai_field_config": {
"message": { "mode": "prompt", "prompt": "Write a greeting for {{vars.name}}" }
}
}Workflow Triggers
Triggers are separate from the workflow graph. Do not store triggers in workflow_graph.
Trigger types:
inbound_message: fires on WhatsApp message (requiresphone_number_id).api_call: fires via Platform API.whatsapp_event: fires on WhatsApp events (requiresevent, optionalphone_number_id).
Use these scripts:
scripts/list-triggers.js <workflow-id>scripts/create-trigger.js <workflow-id> --trigger-type <inbound_message|api_call|whatsapp_event> ...scripts/update-trigger.js --trigger-id <id> --active true|falsescripts/delete-trigger.js --trigger-id <id>scripts/list-whatsapp-phone-numbers.js(to findphone_number_id)
Notes:
- For inbound message triggers, use
phone_number_id(Meta ID). - For whatsapp_event triggers, use
eventlikewhatsapp.message.received. Supported events:whatsapp.message.received,whatsapp.message.sent,whatsapp.message.failed,whatsapp.conversation.created,whatsapp.conversation.ended.whatsapp.message.deliveredandwhatsapp.message.readare not available for new triggers — use webhooks for delivery and read receipts. - For API triggers, no extra fields are required.
Workflow Overview
Key behaviors
- Workflows are automation graphs; an execution moves node-to-node until it waits or ends.
- Trigger inbound_message: if a non-observer execution is running or waiting for that conversation, the message routes to it. If in handoff, workflow processing is skipped. Otherwise, a new execution starts.
- Trigger api_call: starts an execution from the API; context channel is api.
- Trigger whatsapp_event: starts an observer execution for each matching event; observer executions are read-only by default (allow_outbound: false).
Execution states
- running: processing steps.
- waiting: paused waiting for user input (wait_for_response or agent nodes).
- handoff: halted for human takeover; automation does not process inbound messages.
- ended: terminal success.
- failed: terminal error; error_details recorded.
Valid transitions:
- running -> waiting | ended | failed | handoff
- waiting -> running | ended | failed | handoff
- handoff -> running | ended | failed
- ended/failed have no transitions
Reference
Overview
This skill manages workflow graphs, triggers, executions, and functions. Prefer the Kapso CLI local source workflow for normal workflow/function edits; use these Platform API scripts as fallback tools for debugging, unsupported CLI tasks, and API-only environments. Variables CRUD is not supported and will return blocked responses.
For the CLI source-sync workflow and @kapso/workflows, read references/local-workflow-source.md.
Environment
Required env vars:
KAPSO_API_BASE_URL(host only, no/platform/v1, example:https://api.kapso.ai)KAPSO_API_KEY
Scripts
Each script is a single operation. Run with node or bun.
scripts/get-graph.jsscripts/list-workflows.jsscripts/get-workflow.jsscripts/create-workflow.jsscripts/update-workflow-settings.jsscripts/edit-graph.jsscripts/update-graph.jsscripts/validate-graph.jsscripts/list-triggers.jsscripts/create-trigger.jsscripts/update-trigger.jsscripts/delete-trigger.jsscripts/list-executions.jsscripts/get-execution.jsscripts/get-context-value.jsscripts/update-execution-status.jsscripts/resume-execution.jsscripts/variables-list.jsscripts/variables-set.js(blocked)scripts/variables-delete.js(blocked)scripts/list-provider-models.jsscripts/list-execution-events.jsscripts/get-execution-event.jsscripts/list-whatsapp-phone-numbers.js
Platform API endpoints
Implemented calls:
GET /platform/v1/workflowsPOST /platform/v1/workflowsGET /platform/v1/workflows/:idGET /platform/v1/workflows/:id/definition(fetch graph definition)PATCH /platform/v1/workflows/:id(update settings/definition)GET /platform/v1/workflows/:id/variables(workflow variable discovery)GET /platform/v1/workflows/:workflow_id/triggersPOST /platform/v1/workflows/:workflow_id/triggersPATCH /platform/v1/triggers/:idDELETE /platform/v1/triggers/:idGET /platform/v1/workflows/:workflow_id/executionsGET /platform/v1/workflow_executions/:idPATCH /platform/v1/workflow_executions/:idPOST /platform/v1/workflow_executions/:id/resumeGET /platform/v1/workflow_executions/:id/eventsGET /platform/v1/workflow_events/:idGET /platform/v1/provider_modelsGET /platform/v1/whatsapp/phone_numbers(for inbound_message triggers)
Variables CRUD endpoints are not defined for Platform API. Scripts intentionally return blocked for create/update/delete operations.
Workflow execution list endpoints use cursor pagination. Prefer limit, after, and before; responses include a paging object. Do not use page / per_page for new workflow execution or execution event queries.
Workflow graphs: endpoints, shapes, and roundtrips
GET /platform/v1/workflows/:idreturns workflow metadata (includinglock_version) but does NOT includedefinition.GET /platform/v1/workflows/:id/definitionreturns a workflow record that includesdefinition(nodes + edges).PATCH /platform/v1/workflows/:idaccepts eitherworkflow: { ... }orflow: { ... }envelopes.- To update the graph: send
workflow: { definition: <definition> }.
Graph shapes:
get-graph.jsreturns a ReactFlow-style definition that includes extra/computed fields (node.type,data.display_name,edge.id,edge.type).- The API accepts a minimal editable definition (
nodes[].id/position/data.node_type/data.configandedges[].source/target/label) and ignores/strips extra fields.
Source of truth: references/graph-contract.md.
Phone number lookup for triggers
Use scripts/list-whatsapp-phone-numbers.js to find phone_number_id for inbound_message triggers. Do not use /whatsapp_configs (not a Platform API endpoint).
Graph validation rules (local)
The scripts/validate-graph.js script checks:
- Exactly one start node with
id=startanddata.node_type=start. - Unique node IDs and valid edge source/target IDs.
- Non-empty edge labels.
- Only
decidenodes may branch; other nodes may have 0 or 1 outgoingnextedge. - Decide node condition labels must match outgoing edge labels.
Warnings are emitted for unknown node types or extra decide edges. Treat warnings as blockers before you PATCH a graph.
Assets
assets/workflow-linear.json(simple linear example)assets/workflow-decision.json(wait + decide example)
import { readFileSync } from 'node:fs';
import { kapsoConfigFromEnv, kapsoRequest } from './lib/functions/kapso-api.js';
import {
hasHelpFlag,
parseBooleanFlag,
parseFlags,
requireFlag
} from './lib/functions/args.js';
function ok(data) {
return { ok: true, data };
}
function err(message, details) {
return { ok: false, error: { message, details } };
}
function resolveCode(flags) {
if (typeof flags.code === 'string' && flags.code.length > 0) {
return flags.code;
}
if (typeof flags['code-file'] === 'string' && flags['code-file'].length > 0) {
return readFileSync(flags['code-file'], 'utf8');
}
throw new Error('Provide --code or --code-file');
}
async function main() {
const argv = process.argv.slice(2);
if (hasHelpFlag(argv)) {
console.log(
JSON.stringify(
{
ok: true,
usage:
'node scripts/create-function.js --name <name> (--code <js> | --code-file <path>) [--description <text>] [--public-endpoint <true|false>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
},
null,
2
)
);
return 0;
}
try {
const flags = parseFlags(argv);
const name = requireFlag(flags, 'name');
const code = resolveCode(flags);
const payload = { name, code };
const publicEndpoint = parseBooleanFlag(flags, 'public-endpoint');
if (typeof flags.description === 'string' && flags.description.length > 0) {
payload.description = flags.description;
}
if (publicEndpoint !== undefined) {
payload.public_endpoint = publicEndpoint;
}
const config = kapsoConfigFromEnv();
const data = await kapsoRequest(config, '/platform/v1/functions', {
method: 'POST',
body: JSON.stringify({ function: payload })
});
console.log(JSON.stringify(ok(data), null, 2));
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify(err('Command failed', { message }), null, 2));
return 1;
}
}
main().then((code) => process.exit(code));
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/create-trigger.js <workflow-id> --trigger-type <inbound_message|api_call|whatsapp_event> [--phone-number-id <id>] [--event <whatsapp.event>] [--active true|false] [--triggerable-attributes <json>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
function parseBoolean(value) {
if (value === undefined) return undefined;
if (value === true) return true;
const lowered = String(value).toLowerCase();
if (lowered === 'true') return true;
if (lowered === 'false') return false;
return undefined;
}
function parseJson(value) {
if (!value) return undefined;
return JSON.parse(value);
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const triggerType = getFlag(parsed.flags, 'trigger-type');
if (!triggerType) {
printJson(err('trigger-type is required'));
return 2;
}
const active = parseBoolean(getFlag(parsed.flags, 'active'));
const phoneNumberId = getFlag(parsed.flags, 'phone-number-id');
const whatsappConfigId = getFlag(parsed.flags, 'whatsapp-config-id');
const event = getFlag(parsed.flags, 'event');
let triggerableAttributes;
try {
triggerableAttributes = parseJson(getFlag(parsed.flags, 'triggerable-attributes'));
} catch (error) {
printJson(err('Invalid JSON for triggerable-attributes', { message: String(error?.message || error) }));
return 2;
}
const triggerPayload = {
trigger_type: triggerType
};
if (active !== undefined) triggerPayload.active = active;
if (phoneNumberId) triggerPayload.phone_number_id = phoneNumberId;
if (whatsappConfigId) triggerPayload.whatsapp_config_id = whatsappConfigId;
if (event) triggerPayload.event = event;
if (triggerableAttributes) triggerPayload.triggerable_attributes = triggerableAttributes;
const config = loadConfig();
const response = await requestJson(config, {
method: 'POST',
path: `/platform/v1/workflows/${workflowId}/triggers`,
body: { trigger: triggerPayload }
});
if (!response.ok) {
printJson(err('Failed to create trigger', response.raw, false, response.status));
return 2;
}
printJson(ok({ trigger: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import fs from 'fs';
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/create-workflow.js --name <name> [--description <text>] [--definition-file <path> | --definition-json <json>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
function loadDefinition({ filePath, jsonText }) {
if (filePath) {
const raw = fs.readFileSync(filePath, 'utf8');
return JSON.parse(raw);
}
if (jsonText) {
return JSON.parse(jsonText);
}
return undefined;
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const name = getFlag(parsed.flags, 'name');
if (!name) {
printJson(err('name is required'));
return 2;
}
const description = getFlag(parsed.flags, 'description');
const definitionFile = getFlag(parsed.flags, 'definition-file');
const definitionJson = getFlag(parsed.flags, 'definition-json');
if (definitionFile && definitionJson) {
printJson(err('Provide only one of --definition-file or --definition-json'));
return 2;
}
let definition;
try {
definition = loadDefinition({ filePath: definitionFile, jsonText: definitionJson });
} catch (error) {
printJson(err('Failed to parse workflow definition JSON', { message: String(error?.message || error) }));
return 2;
}
const payload = {
workflow: {
name,
description
}
};
if (definition) {
payload.workflow.definition = definition;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'POST',
path: '/platform/v1/workflows',
body: payload
});
if (!response.ok) {
printJson(err('Failed to create workflow', response.raw, false, response.status));
return 2;
}
printJson(ok({ workflow: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/delete-trigger.js --trigger-id <id>',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const triggerId = getFlag(parsed.flags, 'trigger-id');
if (!triggerId) {
printJson(err('trigger-id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'DELETE',
path: `/platform/v1/triggers/${triggerId}`
});
if (!response.ok) {
printJson(err('Failed to delete trigger', response.raw, false, response.status));
return 2;
}
printJson(ok({ deleted: true, status: response.status }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
import { kapsoConfigFromEnv, kapsoRequest } from './lib/functions/kapso-api.js';
import { hasHelpFlag, parseFlags, requireFlag } from './lib/functions/args.js';
function ok(data) {
return { ok: true, data };
}
function err(message, details) {
return { ok: false, error: { message, details } };
}
async function main() {
const argv = process.argv.slice(2);
if (hasHelpFlag(argv)) {
console.log(
JSON.stringify(
{
ok: true,
usage: 'node scripts/deploy-function.js --function-id <id>',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
},
null,
2
)
);
return 0;
}
try {
const flags = parseFlags(argv);
const functionId = requireFlag(flags, 'function-id');
const config = kapsoConfigFromEnv();
const data = await kapsoRequest(config, `/platform/v1/functions/${encodeURIComponent(functionId)}/deploy`, {
method: 'POST',
body: JSON.stringify({})
});
console.log(JSON.stringify(ok(data), null, 2));
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify(err('Command failed', { message }), null, 2));
return 1;
}
}
main().then((code) => process.exit(code));
#!/usr/bin/env node
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from './lib/workflows/args.js';
function sha256(text) {
return createHash('sha256').update(text).digest('hex');
}
function normalizeLineEndings(text) {
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
}
function stripCodeFences(text) {
const trimmed = text.trim();
if (trimmed.startsWith('```')) {
const withoutFirst = trimmed.replace(/^```[a-zA-Z0-9_-]*\n?/, '');
return withoutFirst.replace(/```$/, '').trim();
}
return text;
}
function stripLineNumbers(text) {
return text
.split('\n')
.map((line) => line.replace(/^\s*\d+\s*\|\s?/, '').replace(/^\s*\d+:\s?/, ''))
.join('\n');
}
function normalizeEditText(text) {
return normalizeLineEndings(stripLineNumbers(stripCodeFences(text)));
}
function readFileText(path) {
return readFileSync(path, 'utf8');
}
function loadTextInput(flags, valueFlag, fileFlag) {
const filePath = getFlag(flags, fileFlag);
if (filePath) {
return readFileText(filePath);
}
return getFlag(flags, valueFlag);
}
function isEscaped(text, index) {
let backslashes = 0;
for (let i = index - 1; i >= 0 && text[i] === '\\'; i -= 1) {
backslashes += 1;
}
return backslashes % 2 === 1;
}
function insideJsonString(text, index) {
let inString = false;
for (let i = 0; i < index; i += 1) {
if (text[i] === '"' && !isEscaped(text, i)) {
inString = !inString;
}
}
return inString;
}
function unescapeCLike(text) {
return text
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\');
}
function jsonStringContent(text) {
const json = JSON.stringify(text);
return json.slice(1, -1);
}
function replaceAllOccurrences(content, search, newText) {
let result = '';
let index = 0;
let replacementsInJson = 0;
let replacementsOutsideJson = 0;
while (true) {
const matchIndex = content.indexOf(search, index);
if (matchIndex === -1) {
result += content.slice(index);
break;
}
const insideString = insideJsonString(content, matchIndex);
const replacement = insideString
? jsonStringContent(unescapeCLike(newText))
: newText;
result += content.slice(index, matchIndex) + replacement;
index = matchIndex + search.length;
if (insideString) {
replacementsInJson += 1;
} else {
replacementsOutsideJson += 1;
}
}
return {
content: result,
replacements: replacementsInJson + replacementsOutsideJson,
replacements_in_json_strings: replacementsInJson,
replacements_outside_json_strings: replacementsOutsideJson
};
}
function applyReplacement(content, oldText, newText, replaceAll) {
if (oldText === newText) {
throw new Error('old and new text must be different');
}
const candidates = [
{ label: 'raw', value: oldText },
{ label: 'unescaped', value: oldText.replace(/\\n/g, '\n') },
{ label: 'escaped', value: oldText.replace(/\n/g, '\\n') }
];
const seen = new Set();
const uniqueCandidates = candidates.filter((candidate) => {
if (seen.has(candidate.value)) return false;
seen.add(candidate.value);
return true;
});
for (const candidate of uniqueCandidates) {
if (!candidate.value) continue;
const firstIndex = content.indexOf(candidate.value);
if (firstIndex === -1) continue;
if (replaceAll) {
const replaced = replaceAllOccurrences(content, candidate.value, newText);
return { ...replaced, matchedVariant: candidate.label };
}
const lastIndex = content.lastIndexOf(candidate.value);
if (firstIndex !== lastIndex) {
throw new Error('old text matches multiple locations; use --replace-all or narrow the match');
}
const insideString = insideJsonString(content, firstIndex);
const replacement = insideString
? jsonStringContent(unescapeCLike(newText))
: newText;
const updated = content.replace(candidate.value, replacement);
return {
content: updated,
replacements: 1,
replacements_in_json_strings: insideString ? 1 : 0,
replacements_outside_json_strings: insideString ? 0 : 1,
matchedVariant: candidate.label
};
}
throw new Error('old text not found in workflow graph');
}
function usage() {
return ok({
usage: 'node scripts/edit-graph.js <workflow-id> --expected-lock-version <n> --old <text>|--old-file <path> --new <text>|--new-file <path> [--replace-all]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const expectedLockVersion = getNumberFlag(parsed.flags, 'expected-lock-version')
?? getNumberFlag(parsed.flags, 'lock-version');
if (expectedLockVersion === undefined) {
printJson(err('expected-lock-version is required'));
return 2;
}
const oldInput = loadTextInput(parsed.flags, 'old', 'old-file');
const newInput = loadTextInput(parsed.flags, 'new', 'new-file');
if (!oldInput || !newInput) {
printJson(err('old/new text is required (use --old/--new or --old-file/--new-file)'));
return 2;
}
const replaceAll = getBooleanFlag(parsed.flags, 'replace-all');
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflows/${workflowId}/definition`
});
if (!response.ok) {
printJson(err('Failed to fetch workflow definition', response.raw, false, response.status));
return 2;
}
const workflow = response.data;
const definition = workflow && typeof workflow === 'object' ? workflow.definition : null;
if (!definition || typeof definition !== 'object') {
printJson(err('Workflow definition missing in response', response.raw, false, response.status));
return 2;
}
const currentLock = workflow.lock_version;
if (currentLock !== expectedLockVersion) {
printJson(err('Conflict: workflow was modified. Refetch and retry.', {
expected_lock_version: expectedLockVersion,
current_lock_version: currentLock
}));
return 2;
}
const pretty = JSON.stringify(definition, null, 2);
const beforeHash = sha256(pretty);
const oldText = normalizeEditText(oldInput);
const newText = normalizeEditText(newInput);
let editResult;
try {
editResult = applyReplacement(pretty, oldText, newText, replaceAll);
} catch (error) {
printJson(err(String(error?.message || error)));
return 2;
}
let updatedDefinition;
try {
updatedDefinition = JSON.parse(editResult.content);
} catch (error) {
printJson(err('Invalid JSON after replacement', { message: String(error?.message || error) }));
return 2;
}
const update = await requestJson(config, {
method: 'PATCH',
path: `/platform/v1/workflows/${workflowId}`,
body: {
workflow: {
definition: updatedDefinition
}
}
});
if (!update.ok) {
printJson(err('Failed to update workflow definition', update.raw, false, update.status));
return 2;
}
printJson(ok({
workflow_id: workflowId,
replacements_count: editResult.replacements,
replacements_in_json_strings: editResult.replacements_in_json_strings,
replacements_outside_json_strings: editResult.replacements_outside_json_strings,
matched_variant: editResult.matchedVariant,
workflow_graph_sha256_before: beforeHash,
workflow_graph_sha256_after: sha256(editResult.content),
update: {
id: update.data.id,
name: update.data.name,
status: update.data.status,
lock_version: update.data.lock_version,
updated_at: update.data.updated_at
}
}));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/get-context-value.js <execution-id> --variable-path <path>',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY'],
examples: ['node scripts/get-context-value.js exec_123 --variable-path vars.user_name']
});
}
function getPathValue(obj, path) {
if (!path) return undefined;
const parts = path.split('.').filter(Boolean);
let current = obj;
for (const part of parts) {
if (current && Object.prototype.hasOwnProperty.call(current, part)) {
current = current[part];
} else {
return undefined;
}
}
return current;
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const executionId = parsed.args[0] || getFlag(parsed.flags, 'execution-id');
if (!executionId) {
printJson(err('execution_id is required'));
return 2;
}
const variablePath = getFlag(parsed.flags, 'variable-path');
if (!variablePath) {
printJson(err('variable-path is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflow_executions/${executionId}`
});
if (!response.ok) {
printJson(err('Failed to fetch execution', response.raw, false, response.status));
return 2;
}
const execution = response.data;
const executionContext = execution && execution.execution_context ? execution.execution_context : {};
let path = variablePath;
if (path.startsWith('execution_context.')) {
path = path.replace(/^execution_context\./, '');
}
const value = getPathValue(executionContext, path);
if (value === undefined) {
printJson(err('Path not found in execution_context', { variable_path: variablePath }));
return 2;
}
printJson(ok({ value, variable_path: variablePath, execution_id: executionId }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/get-execution-event.js <event-id> [--event-id <id>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const eventId = parsed.args[0] || getFlag(parsed.flags, 'event-id');
if (!eventId) {
printJson(err('event_id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflow_events/${eventId}`
});
if (!response.ok && response.status === 404) {
printJson(err('Execution event detail endpoint is not available in the Platform API.', {
endpoint: '/platform/v1/workflow_events/:id'
}, true, response.status));
return 2;
}
if (!response.ok) {
printJson(err('Failed to fetch execution event detail', response.raw, false, response.status));
return 2;
}
printJson(ok({
event_id: eventId,
event: response.data
}));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/get-execution.js <execution-id> [--execution-id <id>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const executionId = parsed.args[0] || getFlag(parsed.flags, 'execution-id');
if (!executionId) {
printJson(err('execution_id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflow_executions/${executionId}`
});
if (!response.ok) {
printJson(err('Failed to fetch execution', response.raw, false, response.status));
return 2;
}
printJson(ok({ execution: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
import { kapsoConfigFromEnv, kapsoRequest } from './lib/functions/kapso-api.js';
import { hasHelpFlag, parseFlags, requireFlag } from './lib/functions/args.js';
function ok(data) {
return { ok: true, data };
}
function err(message, details) {
return { ok: false, error: { message, details } };
}
async function main() {
const argv = process.argv.slice(2);
if (hasHelpFlag(argv)) {
console.log(
JSON.stringify(
{
ok: true,
usage: 'node scripts/get-function.js --function-id <id>',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
},
null,
2
)
);
return 0;
}
try {
const flags = parseFlags(argv);
const functionId = requireFlag(flags, 'function-id');
const config = kapsoConfigFromEnv();
const data = await kapsoRequest(config, `/platform/v1/functions/${encodeURIComponent(functionId)}`);
console.log(JSON.stringify(ok(data), null, 2));
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify(err('Command failed', { message }), null, 2));
return 1;
}
}
main().then((code) => process.exit(code));
#!/usr/bin/env node
import { createHash } from 'crypto';
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function sha256(text) {
return createHash('sha256').update(text).digest('hex');
}
function addLineNumbers(text) {
return text
.split('\n')
.map((line, index) => `${String(index + 1).padStart(4, ' ')} | ${line}`)
.join('\n');
}
function usage() {
return ok({
usage: 'node scripts/get-graph.js <workflow-id> [--workflow-id <id>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflows/${workflowId}/definition`
});
if (!response.ok) {
printJson(err('Failed to fetch workflow definition', response.raw, false, response.status));
return 2;
}
const workflow = response.data;
const definition = workflow && typeof workflow === 'object' ? workflow.definition : null;
if (!definition || typeof definition !== 'object') {
printJson(err('Workflow definition missing in response', response.raw, false, response.status));
return 2;
}
const pretty = JSON.stringify(definition, null, 2);
const hash = sha256(pretty);
const withLines = addLineNumbers(pretty);
const trimmedWorkflow = {
id: workflow.id,
name: workflow.name,
description: workflow.description,
status: workflow.status,
lock_version: workflow.lock_version,
message_debounce_seconds: workflow.message_debounce_seconds,
project_id: workflow.project_id,
updated_at: workflow.updated_at
};
printJson(ok({
workflow: trimmedWorkflow,
definition,
workflow_graph_pretty: pretty,
workflow_graph_with_lines: withLines,
workflow_graph_sha256: hash
}));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/get-workflow.js <workflow-id> [--workflow-id <id>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflows/${workflowId}`
});
if (!response.ok) {
printJson(err('Failed to fetch workflow', response.raw, false, response.status));
return 2;
}
printJson(ok({ workflow: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
import { readFileSync } from 'node:fs';
import { kapsoConfigFromEnv, kapsoRequest } from './lib/functions/kapso-api.js';
import { hasHelpFlag, parseFlags, requireFlag, parseJsonValue } from './lib/functions/args.js';
function ok(data) {
return { ok: true, data };
}
function err(message, details) {
return { ok: false, error: { message, details } };
}
function resolvePayload(flags) {
if (typeof flags.payload === 'string' && flags.payload.length > 0) {
return parseJsonValue(flags.payload, 'payload');
}
if (typeof flags['payload-file'] === 'string' && flags['payload-file'].length > 0) {
return JSON.parse(readFileSync(flags['payload-file'], 'utf8'));
}
throw new Error('Provide --payload or --payload-file');
}
async function main() {
const argv = process.argv.slice(2);
if (hasHelpFlag(argv)) {
console.log(
JSON.stringify(
{
ok: true,
usage:
'node scripts/invoke-function.js --function-id <id> (--payload <json> | --payload-file <path>)',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
},
null,
2
)
);
return 0;
}
try {
const flags = parseFlags(argv);
const functionId = requireFlag(flags, 'function-id');
const payload = resolvePayload(flags);
const config = kapsoConfigFromEnv();
const data = await kapsoRequest(config, `/platform/v1/functions/${encodeURIComponent(functionId)}/invoke`, {
method: 'POST',
body: JSON.stringify(payload)
});
console.log(JSON.stringify(ok(data), null, 2));
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify(err('Command failed', { message }), null, 2));
return 1;
}
}
main().then((code) => process.exit(code));
function hasHelpFlag(argv) {
return argv.includes('--help') || argv.includes('-h');
}
function parseFlags(argv) {
const flags = {};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (!arg.startsWith('--')) {
continue;
}
const trimmed = arg.slice(2);
const eqIndex = trimmed.indexOf('=');
if (eqIndex >= 0) {
const key = trimmed.slice(0, eqIndex);
const value = trimmed.slice(eqIndex + 1);
flags[key] = value;
continue;
}
const next = argv[index + 1];
if (!next || next.startsWith('--')) {
flags[trimmed] = true;
continue;
}
flags[trimmed] = next;
index += 1;
}
return flags;
}
function requireFlag(flags, name) {
const value = flags[name];
if (typeof value !== 'string' || value.length === 0) {
throw new Error(`Missing required flag --${name}`);
}
return value;
}
function parseBooleanFlag(flags, name) {
const value = flags[name];
if (value === undefined) {
return undefined;
}
if (value === true) {
return true;
}
const normalized = String(value).toLowerCase();
if (['true', '1', 'yes'].includes(normalized)) {
return true;
}
if (['false', '0', 'no'].includes(normalized)) {
return false;
}
throw new Error(`Invalid boolean for --${name}: ${String(value)}`);
}
function parseEnumFlag(flags, name, allowedValues) {
const value = flags[name];
if (value === undefined) {
return undefined;
}
const normalized = String(value);
if (allowedValues.includes(normalized)) {
return normalized;
}
throw new Error(
`Invalid value for --${name}: ${normalized}. Expected one of: ${allowedValues.join(', ')}`
);
}
function parseJsonValue(value, name) {
if (value === undefined || value === true) {
throw new Error(`Missing required JSON for --${name}`);
}
try {
return JSON.parse(value);
} catch (error) {
throw new Error(`Invalid JSON for --${name}: ${String(error)}`);
}
}
export {
hasHelpFlag,
parseFlags,
requireFlag,
parseBooleanFlag,
parseEnumFlag,
parseJsonValue
};
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
return value;
}
function normalizeBaseUrl(raw) {
return raw.replace(/\/+$/, '');
}
function isLocalhost(hostname) {
return hostname === 'localhost' || hostname === '127.0.0.1';
}
function validateBaseUrl(baseUrl) {
if (!baseUrl) return;
let parsed;
try {
parsed = new URL(baseUrl);
} catch (error) {
throw new Error(`Invalid KAPSO_API_BASE_URL: ${baseUrl}`);
}
if (!process.env.KAPSO_API_ALLOW_LOCALHOST && isLocalhost(parsed.hostname)) {
throw new Error(
`KAPSO_API_BASE_URL points to localhost (${parsed.hostname}). ` +
'Set KAPSO_API_ALLOW_LOCALHOST=true if this is intentional.'
);
}
}
function kapsoConfigFromEnv() {
const baseUrl = normalizeBaseUrl(requireEnv('KAPSO_API_BASE_URL'));
validateBaseUrl(baseUrl);
return {
baseUrl,
apiKey: requireEnv('KAPSO_API_KEY')
};
}
async function kapsoRequest(config, path, init = {}) {
const url = `${config.baseUrl}${path}`;
const headers = new Headers(init.headers || undefined);
headers.set('X-API-Key', config.apiKey);
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
let response;
try {
response = await fetch(url, { ...init, headers });
} catch (error) {
throw new Error(
`Kapso API request failed (network error) url=${url} error=${String(error?.message || error)}`
);
}
const text = await response.text();
if (!response.ok) {
throw new Error(`Kapso API request failed (status=${response.status}) body=${text}`);
}
const contentType = response.headers.get('content-type') || '';
if (!text) {
return {};
}
if (contentType.includes('application/json')) {
return JSON.parse(text);
}
return text;
}
export {
kapsoConfigFromEnv,
kapsoRequest
};
export function parseArgs(argv) {
const flags = {};
const args = [];
let i = 0;
while (i < argv.length) {
const token = argv[i];
if (token.startsWith('--')) {
const trimmed = token.slice(2);
const eqIndex = trimmed.indexOf('=');
if (eqIndex !== -1) {
const key = trimmed.slice(0, eqIndex);
const value = trimmed.slice(eqIndex + 1);
flags[key] = value;
i += 1;
continue;
}
const key = trimmed;
const next = argv[i + 1];
if (!next || next.startsWith('--')) {
flags[key] = true;
i += 1;
} else {
flags[key] = next;
i += 2;
}
continue;
}
args.push(token);
i += 1;
}
return { args, flags };
}
export function getFlag(flags, name) {
const value = flags[name];
if (typeof value === 'string') return value;
return undefined;
}
export function getBooleanFlag(flags, name) {
return Boolean(flags[name]);
}
export function getNumberFlag(flags, name) {
const value = getFlag(flags, name);
if (!value) return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
return value;
}
function normalizeBaseUrl(raw) {
return raw.replace(/\/+$/, '');
}
function isLocalhost(hostname) {
return hostname === 'localhost' || hostname === '127.0.0.1';
}
function validateBaseUrl(baseUrl) {
if (!baseUrl) return;
let parsed;
try {
parsed = new URL(baseUrl);
} catch (error) {
throw new Error(`Invalid KAPSO_API_BASE_URL: ${baseUrl}`);
}
if (!process.env.KAPSO_API_ALLOW_LOCALHOST && isLocalhost(parsed.hostname)) {
throw new Error(
`KAPSO_API_BASE_URL points to localhost (${parsed.hostname}). ` +
'Set KAPSO_API_ALLOW_LOCALHOST=true if this is intentional.'
);
}
}
export function loadConfig(options = {}) {
const requireApi = options.requireApi !== false;
const rawBaseUrl = requireApi ? requireEnv('KAPSO_API_BASE_URL') : (process.env.KAPSO_API_BASE_URL || '');
const baseUrl = rawBaseUrl ? normalizeBaseUrl(rawBaseUrl) : '';
validateBaseUrl(baseUrl);
const apiKey = requireApi ? requireEnv('KAPSO_API_KEY') : (process.env.KAPSO_API_KEY || '');
return { baseUrl, apiKey };
}
function buildUrl(baseUrl, path) {
const trimmed = baseUrl.replace(/\/+$/, '');
const safePath = path.startsWith('/') ? path.slice(1) : path;
return `${trimmed}/${safePath}`;
}
export async function requestJson(config, options) {
const url = new URL(buildUrl(config.baseUrl, options.path));
if (options.query) {
Object.entries(options.query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return;
url.searchParams.set(key, String(value));
});
}
const headers = {
Accept: 'application/json'
};
if (config.apiKey) {
headers['X-API-Key'] = config.apiKey;
}
let body;
if (options.body !== undefined) {
headers['Content-Type'] = 'application/json';
body = JSON.stringify(options.body);
}
let response;
try {
response = await fetch(url.toString(), {
method: options.method,
headers,
body
});
} catch (error) {
return {
ok: false,
status: 0,
error: 'Network error while calling Kapso API',
raw: { message: String(error?.message || error), url: url.toString() }
};
}
const text = await response.text();
let parsed = text;
if (text) {
try {
parsed = JSON.parse(text);
} catch {
parsed = text;
}
}
if (response.ok) {
const data = (parsed && typeof parsed === 'object' && 'data' in parsed)
? parsed.data
: parsed;
return {
ok: true,
status: response.status,
data,
raw: parsed
};
}
const message = (parsed && typeof parsed === 'object' && 'error' in parsed)
? String(parsed.error)
: `HTTP ${response.status}`;
return {
ok: false,
status: response.status,
error: message,
raw: parsed
};
}
export function ok(data) {
return { ok: true, data };
}
export function err(message, details, blocked, status) {
const error = { message };
if (details !== undefined) error.details = details;
if (blocked !== undefined) error.blocked = blocked;
if (status !== undefined) error.status = status;
return { ok: false, error };
}
export function printJson(value) {
// eslint-disable-next-line no-console
console.log(JSON.stringify(value, null, 2));
}
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage:
'node scripts/list-execution-events.js <execution-id> [--event-type <type>] [--limit <n>] [--after <cursor>] [--before <cursor>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const executionId = parsed.args[0] || getFlag(parsed.flags, 'execution-id');
if (!executionId) {
printJson(err('execution_id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflow_executions/${executionId}/events`,
query: {
event_type: getFlag(parsed.flags, 'event-type'),
limit: getNumberFlag(parsed.flags, 'limit'),
after: getFlag(parsed.flags, 'after'),
before: getFlag(parsed.flags, 'before')
}
});
if (!response.ok && response.status === 404) {
printJson(err('Execution events endpoint is not available in the Platform API.', {
endpoint: '/platform/v1/workflow_executions/:id/events'
}, true, response.status));
return 2;
}
if (!response.ok) {
printJson(err('Failed to fetch execution events', response.raw, false, response.status));
return 2;
}
printJson(ok({
execution_id: executionId,
events: response.data,
paging: response.raw?.paging
}));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/list-executions.js <workflow-id> [--status <status>] [--waiting-reason <value>] [--whatsapp-conversation-id <id>] [--created-after <iso>] [--created-before <iso>] [--limit <n>] [--after <cursor>] [--before <cursor>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflows/${workflowId}/executions`,
query: {
status: getFlag(parsed.flags, 'status'),
waiting_reason: getFlag(parsed.flags, 'waiting-reason'),
whatsapp_conversation_id: getFlag(parsed.flags, 'whatsapp-conversation-id'),
created_after: getFlag(parsed.flags, 'created-after'),
created_before: getFlag(parsed.flags, 'created-before'),
limit: getNumberFlag(parsed.flags, 'limit'),
after: getFlag(parsed.flags, 'after'),
before: getFlag(parsed.flags, 'before')
}
});
if (!response.ok) {
printJson(err('Failed to list executions', response.raw, false, response.status));
return 2;
}
printJson(ok({ executions: response.data, paging: response.raw?.paging }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
import { hasHelpFlag, parseFlags, requireFlag } from './lib/functions/args.js';
import { kapsoConfigFromEnv, kapsoRequest } from './lib/functions/kapso-api.js';
function ok(data) {
return { ok: true, data };
}
function err(message, details) {
return { ok: false, error: { message, details } };
}
async function main() {
const argv = process.argv.slice(2);
if (hasHelpFlag(argv)) {
console.log(
JSON.stringify(
{
ok: true,
usage:
'node scripts/list-function-invocations.js --function-id <id> [--status <success|failed>] [--limit <n>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
},
null,
2
)
);
return 0;
}
try {
const flags = parseFlags(argv);
const functionId = requireFlag(flags, 'function-id');
const params = new URLSearchParams();
if (flags.status) params.set('status', flags.status);
if (flags.limit) params.set('limit', flags.limit);
const config = kapsoConfigFromEnv();
const data = await kapsoRequest(
config,
`/platform/v1/functions/${encodeURIComponent(functionId)}/invocations${params.toString() ? `?${params.toString()}` : ''}`
);
console.log(JSON.stringify(ok(data), null, 2));
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify(err('Command failed', { message }), null, 2));
return 1;
}
}
main().then((code) => process.exit(code));
import { kapsoConfigFromEnv, kapsoRequest } from './lib/functions/kapso-api.js';
import { hasHelpFlag } from './lib/functions/args.js';
function ok(data) {
return { ok: true, data };
}
function err(message, details) {
return { ok: false, error: { message, details } };
}
async function main() {
const argv = process.argv.slice(2);
if (hasHelpFlag(argv)) {
console.log(
JSON.stringify(
{
ok: true,
usage: 'node scripts/list-functions.js',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
},
null,
2
)
);
return 0;
}
try {
const config = kapsoConfigFromEnv();
const data = await kapsoRequest(config, '/platform/v1/functions');
console.log(JSON.stringify(ok(data), null, 2));
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify(err('Command failed', { message }), null, 2));
return 1;
}
}
main().then((code) => process.exit(code));
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/list-provider-models.js',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: '/platform/v1/provider_models'
});
if (!response.ok && response.status === 404) {
printJson(err('Provider models endpoint is not available in the Platform API.', {
endpoint: '/platform/v1/provider_models'
}, true, response.status));
return 2;
}
if (!response.ok) {
printJson(err('Failed to fetch provider models', response.raw, false, response.status));
return 2;
}
printJson(ok({
provider_models: response.data
}));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/list-triggers.js <workflow-id> [--workflow-id <id>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflows/${workflowId}/triggers`
});
if (!response.ok) {
printJson(err('Failed to list triggers', response.raw, false, response.status));
return 2;
}
printJson(ok({ triggers: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getBooleanFlag, getNumberFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/list-whatsapp-phone-numbers.js [--per-page <n>] [--page <n>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
function extractPhoneNumbers(payload) {
if (Array.isArray(payload?.data)) return payload.data;
if (Array.isArray(payload?.phone_numbers)) return payload.phone_numbers;
if (Array.isArray(payload)) return payload;
return [];
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: '/platform/v1/whatsapp/phone_numbers',
query: {
per_page: getNumberFlag(parsed.flags, 'per-page'),
page: getNumberFlag(parsed.flags, 'page')
}
});
if (!response.ok) {
printJson(err('Failed to list WhatsApp phone numbers', response.raw, false, response.status));
return 2;
}
const payload = response.data;
const phoneNumbers = extractPhoneNumbers(payload);
printJson(ok({
phone_numbers: phoneNumbers,
raw: payload,
note: 'Use phone_number_id for inbound_message triggers.'
}));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/list-workflows.js [--status <status>] [--name-contains <text>] [--created-after <iso>] [--created-before <iso>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'GET',
path: '/platform/v1/workflows',
query: {
status: getFlag(parsed.flags, 'status'),
name_contains: getFlag(parsed.flags, 'name-contains'),
created_after: getFlag(parsed.flags, 'created-after'),
created_before: getFlag(parsed.flags, 'created-before')
}
});
if (!response.ok) {
printJson(err('Failed to list workflows', response.raw, false, response.status));
return 2;
}
printJson(ok({ workflows: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/resume-execution.js <execution-id> --message <json> [--variables <json>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY'],
examples: [
'node scripts/resume-execution.js exec_123 --message \'{"data":{"text":"hi"}}\''
]
});
}
function parseJson(value, label) {
if (!value) return undefined;
try {
return JSON.parse(value);
} catch (error) {
throw new Error(`Invalid JSON for ${label}: ${String(error?.message || error)}`);
}
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const executionId = parsed.args[0] || getFlag(parsed.flags, 'execution-id');
if (!executionId) {
printJson(err('execution_id is required'));
return 2;
}
let message;
let variables;
try {
message = parseJson(getFlag(parsed.flags, 'message'), 'message');
variables = parseJson(getFlag(parsed.flags, 'variables'), 'variables');
} catch (error) {
printJson(err('Failed to parse JSON', { message: error.message }));
return 2;
}
if (!message || typeof message !== 'object') {
printJson(err('message is required and must be a JSON object'));
return 2;
}
const body = { message };
if (variables) body.variables = variables;
const config = loadConfig();
const response = await requestJson(config, {
method: 'POST',
path: `/platform/v1/workflow_executions/${executionId}/resume`,
body
});
if (!response.ok) {
printJson(err('Failed to resume execution', response.raw, false, response.status));
return 2;
}
printJson(ok({ execution: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/update-execution-status.js <execution-id> --status <ended|handoff|waiting>',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const executionId = parsed.args[0] || getFlag(parsed.flags, 'execution-id');
if (!executionId) {
printJson(err('execution_id is required'));
return 2;
}
const status = getFlag(parsed.flags, 'status');
if (!status) {
printJson(err('status is required'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'PATCH',
path: `/platform/v1/workflow_executions/${executionId}`,
body: { workflow_execution: { status } }
});
if (!response.ok) {
printJson(err('Failed to update execution status', response.raw, false, response.status));
return 2;
}
printJson(ok({ execution: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
import { readFileSync } from 'node:fs';
import { kapsoConfigFromEnv, kapsoRequest } from './lib/functions/kapso-api.js';
import {
hasHelpFlag,
parseBooleanFlag,
parseEnumFlag,
parseFlags,
requireFlag
} from './lib/functions/args.js';
function ok(data) {
return { ok: true, data };
}
function err(message, details) {
return { ok: false, error: { message, details } };
}
function resolveCode(flags) {
if (typeof flags.code === 'string' && flags.code.length > 0) {
return flags.code;
}
if (typeof flags['code-file'] === 'string' && flags['code-file'].length > 0) {
return readFileSync(flags['code-file'], 'utf8');
}
throw new Error('Provide --code or --code-file');
}
async function main() {
const argv = process.argv.slice(2);
if (hasHelpFlag(argv)) {
console.log(
JSON.stringify(
{
ok: true,
usage:
'node scripts/update-function.js --function-id <id> --name <name> (--code <js> | --code-file <path>) [--description <text>] [--public-endpoint <true|false>] [--invoke-response-mode passthrough|wrapped]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
},
null,
2
)
);
return 0;
}
try {
const flags = parseFlags(argv);
const functionId = requireFlag(flags, 'function-id');
const name = requireFlag(flags, 'name');
const code = resolveCode(flags);
const payload = { name, code };
const publicEndpoint = parseBooleanFlag(flags, 'public-endpoint');
const invokeResponseMode = parseEnumFlag(flags, 'invoke-response-mode', ['passthrough', 'wrapped']);
if (typeof flags.description === 'string' && flags.description.length > 0) {
payload.description = flags.description;
}
if (publicEndpoint !== undefined) {
payload.public_endpoint = publicEndpoint;
}
if (invokeResponseMode !== undefined) {
payload.invoke_response_mode = invokeResponseMode;
}
const config = kapsoConfigFromEnv();
const data = await kapsoRequest(config, `/platform/v1/functions/${encodeURIComponent(functionId)}`, {
method: 'PATCH',
body: JSON.stringify({ function: payload })
});
console.log(JSON.stringify(ok(data), null, 2));
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(JSON.stringify(err('Command failed', { message }), null, 2));
return 1;
}
}
main().then((code) => process.exit(code));
#!/usr/bin/env node
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from './lib/workflows/args.js';
function sha256(text) {
return createHash('sha256').update(text).digest('hex');
}
function readFileText(path) {
return readFileSync(path, 'utf8');
}
function normalizeDefinition(input) {
if (!input || typeof input !== 'object') return null;
const record = input;
if (record.ok === true && record.data && typeof record.data === 'object') {
return normalizeDefinition(record.data);
}
if (record.definition && typeof record.definition === 'object') {
return record.definition;
}
if (record.flow && typeof record.flow === 'object') {
const flow = record.flow;
if (flow.definition && typeof flow.definition === 'object') {
return flow.definition;
}
}
if (record.workflow && typeof record.workflow === 'object') {
const workflow = record.workflow;
if (workflow.definition && typeof workflow.definition === 'object') {
return workflow.definition;
}
}
if (record.nodes && record.edges) {
return record;
}
return null;
}
function parseDefinitionInput(raw) {
try {
const parsed = JSON.parse(raw);
return normalizeDefinition(parsed);
} catch {
return null;
}
}
function usage() {
return ok({
usage: 'node scripts/update-graph.js <workflow-id> --expected-lock-version <n> --definition-file <path>|--definition-json <json>',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const expectedLockVersion = getNumberFlag(parsed.flags, 'expected-lock-version')
?? getNumberFlag(parsed.flags, 'lock-version');
if (expectedLockVersion === undefined) {
printJson(err('expected-lock-version is required'));
return 2;
}
const definitionFile = getFlag(parsed.flags, 'definition-file');
const definitionJson = getFlag(parsed.flags, 'definition-json');
if (!definitionFile && !definitionJson) {
printJson(err('definition-file or definition-json is required'));
return 2;
}
const rawDefinition = definitionFile ? readFileText(definitionFile) : definitionJson || '';
const definition = parseDefinitionInput(rawDefinition);
if (!definition) {
const source = definitionFile ? 'definition-file' : 'definition-json';
printJson(err(`Unable to parse workflow definition from ${source}`));
return 2;
}
const config = loadConfig();
const current = await requestJson(config, {
method: 'GET',
path: `/platform/v1/workflows/${workflowId}`
});
if (!current.ok) {
printJson(err('Failed to fetch workflow metadata for lock check', current.raw, false, current.status));
return 2;
}
const currentLock = current.data.lock_version;
if (currentLock !== expectedLockVersion) {
printJson(err('Conflict: workflow was modified. Refetch and retry.', {
expected_lock_version: expectedLockVersion,
current_lock_version: currentLock
}));
return 2;
}
const update = await requestJson(config, {
method: 'PATCH',
path: `/platform/v1/workflows/${workflowId}`,
body: {
workflow: {
definition
}
}
});
if (!update.ok) {
printJson(err('Failed to update workflow definition', update.raw, false, update.status));
return 2;
}
const pretty = JSON.stringify(definition, null, 2);
printJson(ok({
workflow: {
id: update.data.id,
name: update.data.name,
status: update.data.status,
lock_version: update.data.lock_version,
updated_at: update.data.updated_at
},
workflow_graph_sha256: sha256(pretty)
}));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/update-trigger.js --trigger-id <id> --active true|false',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
function parseBoolean(value) {
if (value === undefined) return undefined;
if (value === true) return true;
const lowered = String(value).toLowerCase();
if (lowered === 'true') return true;
if (lowered === 'false') return false;
return undefined;
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const triggerId = getFlag(parsed.flags, 'trigger-id');
if (!triggerId) {
printJson(err('trigger-id is required'));
return 2;
}
const active = parseBoolean(getFlag(parsed.flags, 'active'));
if (active === undefined) {
printJson(err('active is required (true or false)'));
return 2;
}
const config = loadConfig();
const response = await requestJson(config, {
method: 'PATCH',
path: `/platform/v1/triggers/${triggerId}`,
body: { trigger: { active } }
});
if (!response.ok) {
printJson(err('Failed to update trigger', response.raw, false, response.status));
return 2;
}
printJson(ok({ trigger: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
#!/usr/bin/env node
import { loadConfig, requestJson } from './lib/workflows/kapso-api.js';
import { ok, err, printJson } from './lib/workflows/result.js';
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from './lib/workflows/args.js';
function usage() {
return ok({
usage: 'node scripts/update-workflow-settings.js <workflow-id> --lock-version <n> [--name <name>] [--description <text>] [--status <draft|active|archived>] [--message-debounce-seconds <n>] [--inbound-message-read-mode <disabled|read_only|read_with_typing>]',
env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
});
}
async function main() {
const parsed = parseArgs(process.argv.slice(2));
if (getBooleanFlag(parsed.flags, 'help') || getBooleanFlag(parsed.flags, 'h')) {
printJson(usage());
return 0;
}
const workflowId = parsed.args[0] || getFlag(parsed.flags, 'workflow-id');
if (!workflowId) {
printJson(err('workflow_id is required'));
return 2;
}
const lockVersion = getNumberFlag(parsed.flags, 'lock-version');
if (lockVersion === undefined) {
printJson(err('lock-version is required'));
return 2;
}
const payload = {
workflow: {
lock_version: lockVersion
}
};
const name = getFlag(parsed.flags, 'name');
const description = getFlag(parsed.flags, 'description');
const status = getFlag(parsed.flags, 'status');
const messageDebounce = getNumberFlag(parsed.flags, 'message-debounce-seconds');
const inboundMessageReadMode = getFlag(parsed.flags, 'inbound-message-read-mode');
if (name) payload.workflow.name = name;
if (description) payload.workflow.description = description;
if (status) payload.workflow.status = status;
if (messageDebounce !== undefined) payload.workflow.message_debounce_seconds = messageDebounce;
if (inboundMessageReadMode) payload.workflow.inbound_message_read_mode = inboundMessageReadMode;
const config = loadConfig();
const response = await requestJson(config, {
method: 'PATCH',
path: `/platform/v1/workflows/${workflowId}`,
body: payload
});
if (!response.ok) {
printJson(err('Failed to update workflow', response.raw, false, response.status));
return 2;
}
printJson(ok({ workflow: response.data }));
return 0;
}
main().catch((error) => {
printJson(err('Unhandled error', { message: String(error?.message || error) }));
process.exit(1);
});
Related skills
Forks & variants (1)
Automate Whatsapp has 1 known copy in the catalog totaling 238 installs. They canonicalize to this original listing.
- gokapso - 238 installs
How it compares
Choose automate-whatsapp for Kapso flow builder patterns with button-reply branching and agent nodes rather than simple webhook echo bots.
FAQ
What is the preferred workflow edit path?
Use kapso link, pull, edit workflow.ts locally, kapso build, then kapso push with optional --dry-run.
How do I fix lock_version conflicts?
Re-fetch the graph with get-graph.js, re-apply edits, and update with the new expected lock_version.
What handler shape do Kapso functions require?
async function handler(request, env) returning a Response; no export default or arrow functions.
Is Automate Whatsapp safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.