Start here

Quickstart

The goal of this page is one thing: see a task stop at an approval and then move because you said so. That single loop is the whole product. Everything else is variations on it.

No frontend, no model key, no Docker. Roughly ten minutes.

1. Run the kernel

# one-liner: clone + set up
curl -fsSL https://mycelai.dev/init | bash

# or from a checkout
cd kernel && npm i && npm run dev
# → mycel-harness v0.1 on http://localhost:4000

The boot line tells you what mode you are actually in. Read it:

mycel-harness v0.1 on http://localhost:4000
  [sandbox=local store=memory model=anthropic/claude-opus-4-8 queue=inline]
  • store=memory — nothing survives a restart. Set MYCEL_DATABASE_URL when that starts mattering.
  • queue=inline — this process runs every task it receives. Correct on a laptop, a ceiling in production.
  • sandbox=local — a temp directory on your machine. Convenient, and not an isolation boundary.

Below the banner the kernel prints an ephemeral MYCEL_API_KEY (prefix msk_) and an owner email and password for the portal. Copy both. They are regenerated on every boot until you set MYCEL_API_KEY, MYCEL_OWNER_EMAIL and MYCEL_OWNER_PASSWORD.

Start without a model key. The mock runtime emits the full event shape — steps, tokens, tool calls, cost, a schema-valid output — with no OpenCode and no provider:

MYCEL_RUNTIME=mock npm run dev

Only the exact string mock selects it. Anything else, including MOCK, silently means the real runtime.

2. Create a task

A task is always wedge + task_type + input. The shipped invoice-chaser wedge is the smallest example in the repo: one task type, chase_invoice, which decides which rung of the dunning ladder an overdue invoice is on and drafts the message. It never sends on its own — send is a required approval at high risk.

export MYCEL_API_KEY=<printed-key>

curl -sX POST http://localhost:4000/v1/tasks \
  -H "authorization: Bearer $MYCEL_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "wedge": "invoice-chaser",
    "task_type": "chase_invoice",
    "input": {
      "invoice_number": "INV-1042",
      "currency": "USD",
      "amount_due": 240000,
      "due_date": "2025-04-01",
      "days_overdue": 12,
      "partially_paid": false
    }
  }'

The task type declares no input_schema, so nothing validates this on the way in — the skill reads it. Those field names are not arbitrary, though: when the dunning sweep spawns the same task type itself it builds exactly this shape (chaseTaskInput in dunning.ts), and days_overdue is the number the next_step workflow branches on to pick the rung.

You get back a Task with an id and status: "queued". Save the id. Sending an Idempotency-Key header makes the call safe to retry — a repeat returns the same task instead of running the work twice.

When this 400s

ErrorCause
unknown wedgeNo wedges/<slug>/wedge.jsonunder the kernel's working directory. A malformed wedge.json looks identical — it is parsed silently and returns nothing.
unknown task_typeThe key is missing from task_types in the manifest.
wedge "x" is not enabled for this project (403)The project has a non-empty wedges allowlist. Empty means all.
429Rate limit on task creation — 120 per minute per credential, MYCEL_RATE_MAX.

3. Watch the stream

curl -N http://localhost:4000/v1/tasks/<id>/events \
  -H "authorization: Bearer $MYCEL_API_KEY"

A run that hits an approval looks like this:

  1. task.created — emitted once the sandbox is up, so it is event id 1 but not the moment you posted
  2. step.started, then tool.called / tool.result and token.delta as the agent works
  3. approval.requested — the run blocks here; task status becomes awaiting_approval
  4. after you decide: approval.resolved, then output.validated, artifact.created, task.finished

Every event carries a monotonic per-task id which is also the SSE event id. Reconnect with Last-Event-ID: N to replay everything after N. Full rules: Events & the stream.

If the stream just sits there. There is no heartbeat frame, so silence is normal while the agent thinks — it does not mean the connection is dead. If it is silent for minutes, check GET /v1/tasks/:id: status awaiting_approval means it is waiting for you, and a pending approval that nobody resolves is abandoned after five minutes with decision expired, which ends the task.

4. Resolve the approval

# what is waiting
curl -s "http://localhost:4000/v1/approvals?status=pending" \
  -H "authorization: Bearer $MYCEL_API_KEY"

# approve, correcting the draft on the way through
curl -sX POST http://localhost:4000/v1/approvals/<approval-id>/approve \
  -H "authorization: Bearer $MYCEL_API_KEY" \
  -H "content-type: application/json" \
  -d '{"edited": {"body": "Hi Sam — quick nudge on INV-1042, $2,400, due April 1…"}}'

The task resumes at running. Rejecting does not skip the step — it ends the whole task with status rejected. Deciding twice returns 409 already approved.

The edited payload is the interesting part: it both changes what actually gets sent and is written back as a correction knowledge item for the wedge. See Approvals for where that applies and where it does not.

5. Open the portal (optional)

cd portal
npm i
cp .env.local.example .env.local
# MYCEL_KERNEL_URL=http://localhost:4000
npm run dev
# → http://localhost:3000

Log in with the owner credentials from the boot banner. Approvals, task timelines, connections and knowledge all have a UI here, so you do not have to keep curling.

Ports

ServiceDefaultEnv
Kernel4000PORT
Portal3000PORT
OpenCode, inside the sandbox4444OPENCODE_PORT

What to read next

You have run someone else's wedge. Next you either wrap it in your own UI — Your first product — or make it yours by writing a wedge of your own, which starts at Wedges.