Composing Agents From Four Primitives: Tool, Skill, Knowledge, Toolbox
An agent in Matrix is assembled, not coded — mix four primitives and the runtime composes one coherent tool surface and prompt, with no per-feature wiring.
Most agent frameworks make you code an agent. You wire a tool registry, paste a system prompt, bolt on a retriever, thread a memory store through your request handler, and then do it all again — slightly differently — for the next agent. The agent becomes a snowflake. The wiring becomes the product.
Matrix takes the opposite stance: an agent is assembled, not coded. You pick from four composable primitives, attach any combination, and the runtime fuses them into one coherent tool surface and one prompt per turn. The agent end has no per-feature wiring. Attach a thing — it composes.
This post walks the four primitives and then the single composition path that makes them add up.
The four primitives
Every capability an agent has comes from one of four things you mix and match.
1. Tool — a single callable
The atom. A Tool is one callable function the model can invoke. It comes in three transports:
- HTTP — a remote endpoint (your API, a SaaS webhook).
- MCP method — a method exposed by an external Model Context Protocol server.
- INTERNAL built-in — an in-JVM implementation like
web_search, routed throughBuiltinToolRegistryinstead of an HTTP callback.
A tool is the smallest unit you can attach. Reference it from agent.tools and the model can call it. That's the whole contract.
2. Skill — a behaviour bundle
A Skill is the reusable unit of behaviour. Where a tool is one callable, a skill packages everything an agent needs to be good at a job:
systemPromptBlock— a prompt augmentor concatenated into every agent that adopts the skill.tools— Tool entity refs that merge into the agent's surface.mcpServers— MCP server refs, same idea.requiredContactFields— CSV unioned into the agent's memory checklist of what to learn about the contact.builtinTools— which of the memory built-ins to expose (e.g.set_contact_birth_place).- bundled
SkillFilerows — scripts, templates, reference material that travel with the skill.
The payoff: a skill is portable. Attach one skill and you get its prompt block, its HTTP tools, its MCP servers, its required-field selection, and its bundled files — all at once. And because Matrix speaks the Anthropic Agent Skills format, any GitHub repo that publishes a SKILL.md is one POST from being a skill in your org:
POST /api/orgs/{slug}/skills/import
{ "url": "https://github.com/anthropics/skills/tree/main/skills/pdf" }
The importer parses the frontmatter, uses the markdown body as the systemPromptBlock, and writes every other file in the directory as a SkillFile row. Those files matter for composition — more on that below.
3. Knowledge — a per-org RAG corpus
A Knowledge is a retrieval corpus scoped to your org. You drag in .md / .txt / .html / .pdf; ingestion parses (Jsoup for HTML, PDFBox for PDF), chunks at ~2000 chars with 200-char overlap, embeds each chunk, and stores it as a KnowledgeChunk row on Neo4j's native HNSW vector index — the same index that powers memory recall, so there's no new infra to stand up.
The composable part is what happens when you attach a corpus to an agent: the agent automatically gets a search_knowledge(knowledge_key, query, top_k) tool whose description enumerates the available corpora and returns ranked chunks with a sourceRef for citation. You never write retrieval plumbing. Flip graphragEnabled and ingestion also extracts an entity/relation graph per chunk for one-hop multi-hop retrieval. We go deep on the auto-attach mechanics in Auto-Wired Retrieval.
4. The built-in toolbox — batteries included
Seven INTERNAL-transport tools seeded into every org on boot:
web_search (DuckDuckGo, no API key) · fetch_url · bash · file_read · file_write · file_list · grep
All sandbox-scoped per (org, agent) under /tmp/matrix-sandbox/, with symlink-escape rejection. You don't attach these one at a time — the seeded toolkit-essentials skill groups all seven, so one skill attachment gives an agent a working shell, a filesystem, and a search engine.
A note on trust: the sandbox is path-restricted only — no cgroups/seccomp isolation. A malicious skill could
curlan exfiltrator. Don't attachbashto agents that adopt skills from sources you don't trust.
How they compose: one canonical path
Here's the part that makes "assembled, not coded" true. Every primitive resolves through a single method — AgentToolSurface.composeForCaller — and it runs per turn. There is no separate code path for "this agent has knowledge" or "this agent has a skill". There is one path that unions everything the agent currently has.
For each turn, composeForCaller unions:
agent.tools— the agent's direct HTTP / CLIENT_DISPLAY tool refs.agent.mcpServers— directly attached MCP servers.skill.toolsfrom every attached skill — each skill's tool refs, merged in.BuiltinToolRegistrylookups — for any INTERNAL-transport tool that resolved (web_search, bash, …).- the auto-attached
search_knowledge— only whenagent.knowledgeis non-empty. - the memory built-ins — gated by the keys skills requested via
builtinTools. - the ambient
get_current_time— always, so the model doesn't drift to its training-cutoff date.
One union. One tool surface. And before composition even begins, SkillSandboxMaterializer writes every SkillFile row for the attached skills into the agent's sandbox — so when the bash tool runs, it sees the skill's bundled scripts as if the folder were checked out locally on disk.
The prompt is assembled the same way: the agent's own systemPromptBlock plus each attached skill's systemPromptBlock plus the memory profile block plus the current-date header. Attach a skill and its prompt augmentor and its tools land together — no second step where you remember to also paste the prompt.
Why "per turn" is the whole game
Because composition happens on every turn from the live entities, the agent has no baked-in state to drift. Detach a knowledge corpus and search_knowledge simply isn't in the next turn's surface. Add a skill and its tools appear on the very next turn — no redeploy, no restart, no re-registration. The agent definition is just a set of references; the runtime resolves them fresh each time.
This is also why the same agent behaves identically across channels. Text chat, real-time voice, and autonomous background tasks all drive the same composed prompt and the same composed tool surface. Parity isn't a feature you maintain — it's a consequence of there being one composition path.
The shape of an assembled agent
Put concretely, here's what "no per-feature wiring" looks like in practice. An admissions-counsellor agent might be assembled from:
Agent
├── systemPrompt: "You are a warm admissions counsellor…"
├── tools: [check_seat_availability (HTTP)]
├── mcpServers: []
├── skills: [toolkit-essentials, ← web_search + bash + file_*
│ pdf-skill (imported)] ← SKILL.md prompt + bundled scripts
├── knowledge: [course-catalog-2026] ← auto-attaches search_knowledge
└── requiredCallerFields: name,phone,intended_program
Nobody wrote retrieval code. Nobody registered a tool array. Nobody wired the PDF skill's scripts into a sandbox. Each line is a reference to a primitive; composeForCaller does the assembly. To change the agent's behaviour, you change a reference — not a fork of the platform.
That separation is deliberate and it goes all the way down: personas live in data, never in code. We make the full case for that in Personas as Data, Not Code.
The takeaway
Four primitives — Tool, Skill, Knowledge, Toolbox — and one canonical composition path that unions them per turn into a single coherent prompt and tool surface. The agent end carries no per-feature wiring: you attach references, and AgentToolSurface.composeForCaller resolves them fresh on every turn, identically across chat, voice, and autonomous tasks. Adding a capability is attaching a primitive, not editing a code path. Removing one is detaching it.
That's the difference between a framework where every agent is a hand-wired snowflake and a platform where agents are composed.
Try it
Open /orgs/{slug}/admin/agents → New agent, then use the drawer pickers to attach a skill and a knowledge corpus and watch the tool surface assemble itself — no code. Or create a workspace and import a community skill with a single POST to start composing your first agent from primitives.
Build your first agent on Matrix
Spin up a workspace, wire up tools and knowledge, give your agent a voice, and talk to it in real time — no agent code required.