Actions and action sets¶
An action is one concrete thing the agent can do: write a file, search the web, send a Slack message, run a shell command. The action registry is the agent's entire vocabulary: if no action exists for something, the agent cannot do it. Action sets group related actions so each task carries only the vocabulary it needs.
Overview¶
Three layers, each answering a different question:
| Layer | Question it answers | When it's decided |
|---|---|---|
| Registry | What can this agent do at all? | At startup (import time), plus whenever MCP servers connect |
| Action sets | What can this task do? | Once, at task creation (adjustable mid-task) |
| Router | What happens this turn? | Every iteration of the agent loop |
CraftBot ships 1,176 actions: 56 core actions (task management, files, web, documents, media, shell, scheduling, memory, messaging) and 1,120 integration actions under app/data/action/integrations/<service>/. The full catalogue is in the actions reference.
Anatomy of an action¶
An action is a Python function with an @action decorator that registers it at import time. The metadata (not the implementation) is what the selection LLM sees, so the description field does most of the work:
| Field | What it does |
|---|---|
name |
Unique identifier, e.g. web_search |
description |
What the LLM reads to decide when to pick this action |
input_schema / output_schema |
Parameter and result contracts, shown to the LLM |
action_sets |
Which sets contain this action (an action can be in several) |
mode |
Interface visibility (which interface contexts offer the action); "ALL" means everywhere |
execution_mode |
"internal" (in-process) or "sandboxed" (isolated venv) |
platforms |
windows / linux / darwin / all; see platform dispatch below |
requirement |
pip packages the action needs; installed automatically before it runs |
parallelizable |
Whether it may run alongside other actions in one turn (False for writes, state changes, send_message) |
irreversible |
Marks side effects that can't be undone once they reach the outside world (send email, post publicly) |
default, test_payload |
Always-available flag (legacy; prefer action_sets), and data for simulated test runs |
Irreversible actions get a crash guard. Before an irreversible=True action executes, its intent is durably recorded in an activity ledger. After execution the outcome is recorded too. If CraftBot crashes between the send and the record, the guard refuses a blind re-execution and surfaces a warning instead. The agent verifies or asks you rather than sending your email twice.
Platform dispatch. The registry stores implementations per platform. One logical name like run_shell can have Windows, macOS, and Linux variants. Lookup picks the current platform's implementation and falls back to the generic all one.
Action sets¶
Sets are labels declared in each action's metadata. The registry discovers them dynamically by scanning, so a custom action declaring action_sets=["my_tools"] creates the my_tools set with no other registration step. The built-in sets:
| Set | Contains |
|---|---|
core |
35 always-included actions: messaging, task control, file operations, search, web research, shell and HTTP, clipboard, scheduling, integration management, skill and set management, memory search, and sub-agent spawning |
document_processing |
PDF reading, editing, and conversion, markdown conversion, OCR, image description, video understanding |
image |
describe_image, generate_image, perform_ocr, understand_video |
video |
generate_video, perform_ocr, understand_video |
content_creation |
generate_image, generate_video |
scheduler |
Schedule management actions (also present in core) |
proactive |
Recurring-task management for PROACTIVE.md plus schedule management |
living_ui |
The 7 Living UI lifecycle actions (scaffold, launch, restart, import, data access) |
Connected integrations contribute their own sets, and each connected MCP server becomes a set named mcp_<server> (see below).
How a task gets its actions¶
When a task starts, one LLM call selects both the skills and the action sets for it, based on the task description: a report-writing task gets document_processing, a Living UI build gets living_ui. Sets recommended by the selected skill are merged in automatically, core is always included, and the union is compiled into a static action list that the task carries for its lifetime.
This compile-once design is deliberate: during execution there is no retrieval step and no searching for tools. The task's vocabulary is a fixed list the router reads directly.
The list can still change, though. Mid-task, the agent can call list_action_sets, add_action_sets, and remove_action_sets (all in core, so always available) to expand or trim its own vocabulary when it discovers it needs something. These calls appear in the action panel when a task discovers mid-way that it needs another capability.
Per-turn selection¶
Every iteration of the agent loop, the router makes one LLM call that returns reasoning plus a list of one or more actions:
{"reasoning": "...", "actions": [{"action_name": "web_search", "parameters": {...}},
{"action_name": "task_update_todos", "parameters": {...}}]}
Rules applied to that list before execution:
- Parallel execution. Multiple actions in one decision run concurrently, up to 10 per batch.
- Non-parallelizable wins alone. If any selected action has
parallelizable=False, it runs by itself and the rest are dropped with an error the agent sees next turn. - Format errors retry, then abort. Malformed LLM output gets up to 3 retries with the parse error fed back. After that the task aborts rather than wasting tokens.
- Conversation mode is narrow. Outside a task, the candidates are only
send_message,task_start,ignore, plus messaging actions for connected platforms. Real work requires a task.
Each execution logs action_start / action_end events to the task's event stream, which is what the action panel in the browser renders live.
Internal vs sandboxed execution¶
execution_mode decides where the function body runs:
internalruns in the CraftBot process. Used by actions that touch agent state (task management, messaging, memory) and by MCP tools. Declaredrequirementpackages are pip-installed into the main environment before the first run.sandboxedruns in a separate process using a persistent virtual environment (~/.craftbot/sandbox_venv, created lazily on first use and reused). Requirements install into the sandbox venv once and persist across calls. A timeout kills runaway executions.
Either way, the action returns a dict that flows back into the event stream as the observation for the next turn.
MCP tools join the same registry¶
Servers configured in app/config/mcp_config.json (or managed via the /mcp command and Settings) expose tools that are converted into ordinary actions at connect time: each tool becomes an action named mcp_<server>_<tool>, its JSON Schema becomes the input_schema, and all of a server's tools land in an action set named mcp_<server> (configurable per server). From that point nothing downstream knows the difference: MCP sets appear in task-creation selection, MCP actions appear in the router's candidates, and disabling a server unregisters its actions. Details and server setup: MCP.
Implementation files
Registry and decorator: agent_core/core/action_framework/registry.py. Set compilation: app/action/action_set.py. Set/skill selection at task creation: app/internal_action_interface.py. Per-turn routing: agent_core/core/impl/action/router.py. Execution: agent_core/core/impl/action/manager.py and executor.py. MCP conversion: agent_core/core/impl/mcp/adapter.py.
Next¶
- Actions reference: the full catalogue, action by action
- Write a custom action: one file, one decorator
- Skills: strategy injected on top of the action vocabulary
- Agent loop: where selection and execution sit in the cycle
- MCP: connecting external tool servers