Concepts
Wedges
You have a service in your head. Somewhere in it is a repeated unit of work with a beginning and an end — chase this invoice, reply to this lead, close this month. A wedge is that unit written down: what it takes in, what it must produce, what it is allowed to touch, and what has to stop for a human.
It is configuration, not code. You do not write an agent loop, a queue, an SSE stream or a secrets vault. One horizontal kernel, many verticals — and the reason a vertical does not go generic is entirely in the wedge.
Anatomy
wedges/<slug>/ wedge.json # the contract: task types, schemas, approvals, policy skills/*.md # HOW the job is done knowledge/*.md # WHAT is true — seed grounding, editable at runtime workflows/*.mjs # optional: deterministic code the agent may call
The folder lives under MYCEL_WEDGES_DIR, which defaults to wedges/ beside the kernel's working directory. Drop a folder in, and POST /v1/tasks can reach it — there is no registration step and no build.
The smallest wedge that works
invoice-chaser, shipped in the repo. One task type, one decision, and every way of touching money gated separately. This is the real manifest, with only the workflows block elided — it declares one function, next_step, covered further down:
{
"wedge": "invoice-chaser",
"title": "Accounts-Receivable Chaser",
"task_types": {
"chase_invoice": {
"description": "Given an overdue invoice and the client's history, draft the right next dunning step and (on approval) send it.",
"harness": {
"shape": "decide",
"max_runtime_s": 240,
"max_cost_usd": 0.5,
"strict_output": true,
"needs_connections": true,
"temperature": 0.1,
"steps": 30,
"skills": ["chase-politely"],
"instructions": ["knowledge/dunning-policy.md"]
},
"output_schema": {
"type": "object",
"properties": {
"step": { "type": "string", "enum": ["reminder", "firm_reminder", "final_notice", "hold"] },
"channel": { "type": "string", "enum": ["email", "none"] },
"message": { "type": "string" },
"reasoning": { "type": "string" }
},
"required": ["step", "channel", "message"]
}
}
},
"tools": [],
"connections": ["billing-email", "stripe"],
"approvals": [
{ "action": "send", "risk": "high", "required": true },
{ "action": "charge", "risk": "high", "required": true },
{ "action": "refund", "risk": "high", "required": true }
],
"skills": ["chase-politely"],
"knowledge": ["dunning-policy.md"],
"cases": {
"stages": ["outstanding", "reminded", "firm", "final_notice", "paid", "written_off"],
"initial": "outstanding"
},
"workflows": [ … ],
"policy": {
"auto_approve": [
{ "action": "email:send_reminder", "max_per_task": 3, "max_per_day": 50 }
]
}
}Note what is not there. No input_schema — nothing validates task input on the way in, the skill reads it. No intake — this wedge asks the founder nothing up front, so its intake coverage reads 100% until the agent discovers a gap. No model: model choice comes from the tier and the deployment, not the manifest.
Why the output schema is the important field
It is what stops the agent narrating instead of working. If the output does not validate, the task fails — output.validated with ok: false, then task.finished with status failed. A confident paragraph where a number was required is a failure, not a partial success, and your UI will show it as one.
Make the schema demand the thing you would check by hand. If the deliverable is a yield calculation, require the number. If it is a decision, make it an enum. Requiring draft_reply is what forces a draft to exist for the human to approve.
The validator is not full JSON Schema. It checks types, object properties, required, array items and enum. It ignores additionalProperties, oneOf, pattern, minimum and format. Some shipped manifests carry additionalProperties: false and it does nothing today — do not rely on it to keep extra keys out.
The fields, and when you need them
| Field | Reach for it when |
|---|---|
task_types | Always. Everything else is optional. |
approvals | An action must stop for a human. required: true also injects the action name into the sandbox tool gate. |
policy.auto_approve | A specific low-risk action is costing you real time. Absent means everything is gated — the safe default. |
connections | The work touches the outside world. Names must match registered connections. |
cases | The work has a lifecycle across many tasks — outstanding → reminded → paid. Declare stages and an initial. |
workflows | Something must be exactly right — sales tax, yields, dunning steps. Never let the model do arithmetic that appears on an invoice. |
intake | You know which questions a new customer must answer. Seeds the intake queue. |
skills / knowledge | Naming specific files. Omit either field to load every file in the folder. |
Workflows: the escape hatch from probabilistic maths
A workflow is your own .mjs file. The agent chooses when to call it and with what arguments; it never chooses the logic. Arguments are validated against the declared input_schema, the result against output_schema, with a 5s timeout (MYCEL_WORKFLOW_TIMEOUT_MS) and a 128KB result cap.
Note where it runs: in the harness process, at full trust. It is your code, so that is fine, but it is not sandboxed. Do not have a workflow do something the agent could talk it into doing badly.
Three shapes worth copying
Outbound that stops when a human answers — gtm-operator
A schedule runs advance_sequences; each due prospect gets an outreach_touch. The decision to send is not the model's: a next_touch workflow owns it, and its description tells the agent always call this before drafting a follow-up. It stops on a reply, on a booking and on an opt-out — three ways a campaign must go quiet that you do not want a model reasoning about under a deadline.
Research to artifact — geo-monitor
sweep checks how often a client is cited by answer engines and returns queries_checked, mentions, share_of_voice_pct and the gaps; weekly_report turns a week of those into something a client reads. The percentage is computed by a share_of_voice workflow rather than by the agent, and the manifest says exactly why: it is the number the client is billed against, and a model must not be free to round it favourably. That is the line between judgment and mechanics — prose is the model's job, arithmetic is not. Its policy auto-approves only read-prefixed actions, capped at 500 a day, so a sweep never becomes a send.
Money, tightly held — invoice-chaser
A schedule sweeps overdue invoices and creates one task each. Cases track outstanding → reminded → firm → final_notice → paid → written_off. send, charge and refund are three separate required approvals at high risk. Stripe is a Composio connection, so no key is ever near the sandbox. Policy auto-approves only the first-line reminder, three per task and fifty per day.
How to build one
- Author it by conversation. Run the
mycel-wedge-builderskill in Claude Code — it interviews you and writes the manifest, skills and seed knowledge in your words. Faster and better than starting from a blank file. - Register the connections it names (Connections & secrets).
- Bind a channel if the work arrives rather than being requested.
- Run it against the mock runtime first —
MYCEL_RUNTIME=mocksynthesises a schema-valid output, which proves your schemas and your UI before you spend a penny on tokens. - Then improve it without redeploying — knowledge and corrections are runtime data. Only schema and procedure changes need a release.
Failure modes
unknown wedgeand the folder is definitely there. Malformedwedge.jsonis swallowed silently and looks exactly like a missing file. Run the JSON through a parser.- A knowledge file listed in the manifest never loads. Knowledge names are verbatim — no
.mdis appended, unlike skills. - Tasks fail on validation. Usually the schema demands something the skill never told the agent to produce. Fix them together.
- A policy rule you added is being ignored. Only the first matching rule applies. A broad rule above a narrow one wins.
- Approvals fire on things you did not expect. The sandbox gate matches tool names by substring against
send,book,deleteand others, sobookkeeping_readstops. Over-gating is the intended direction.
What to read next
Events & the stream — what a running wedge emits, and the rules your UI has to follow to render it.