| title | Changelog |
|---|---|
| description | Stay up to date with UseZombie product updates, new features, and improvements. |
<Update label="May 3, 2026" tags={["Feature", "Breaking", "API", "CLI", "UI"]}>
Tenants can now run zombie events against their own LLM provider account ("BYOK") instead of the platform-managed default. Both modes share the same gate, the same metering, and the same credit pool — they differ in drain rate, not eligibility. Every new tenant gets a $10 starter grant; the gate trips on the next event after exhaustion (no in-flight kill).
- Provider posture is tenant-scoped, not workspace-scoped. A new
core.tenant_providersrow pins one of two postures per tenant:platform(we charge from your zombie credits) orbyok(your provider, your API key, our flat per-event overhead). The legacyPUT|GET|DELETE /v1/workspaces/{ws}/credentials/llmroute has been removed; pre-v2.0 carve-out applies — the URL returns404, not410, and there is no compat shim. - Two-debit metering. Each event yields up to two charge rows in
core.zombie_execution_telemetry: areceivecharge committed at gate-pass and astagecharge committed before execution and updated post-run with token counts. The dashboard groups them by event. - Per-token rates. The public
_um/<key>/model-caps.jsonendpoint now carriesinput_cents_per_mtokandoutput_cents_per_mtokper model. The API server populates a process-local cache fromcore.model_capsat boot —compute_stage_chargereads it on the hot path. - Starter grant on signup.
tenant_billing.insert_starter_grantruns in the tenant-create transaction. Existing tenants are unaffected; the grant ships once per tenant, never re-applied.
GET /v1/tenants/me/provider— resolved config (mode, provider, model, context cap, credential ref). Theapi_keyis never returned.PUT /v1/tenants/me/provider— flip to BYOK by passing{ "mode": "byok", "credential_ref": "<vault-name>" }. Optionalmodeloverride; otherwise the model in the credential body is used. Tenant-admin only (403 otherwise).DELETE /v1/tenants/me/provider— equivalent toPUT mode=platform. Resets to the platform default and surfaces a low-balance warning if applicable.
GET /v1/tenants/me/billing— plan + balance snapshot (already shipped; unchanged).GET /v1/tenants/me/billing/charges?limit=— newest-first credit-pool charge rows (one per(event_id, charge_type)). Backs the Settings → Billing Usage tab. Note: REST §1 forbids/usageas a final segment (not a plural noun), so the resource is/charges— each row is literally a charge.
PUT|GET|DELETE /v1/workspaces/{workspace_id}/credentials/llm— never wired to a runtime resolver. Use/v1/tenants/me/providerplus a credential stored in the workspace vault.
zombiectl tenant provider {get|set|reset}— manage the tenant's active LLM posture.set --credential <name> [--model <override>]requires the credential name explicitly so the link to your vault entry is unmistakable.resetwarns if your credit balance falls below 100¢.zombiectl billing show [--limit N] [--json]— read-only dashboard. Prints the formatted balance plus the last N events (default 10) with receive / stage / total cents columns. Footer points athttps://app.usezombie.com/settings/billing. Nopurchase/topup/configuresubcommands in v2.0 — Stripe lands in v2.1.
- Settings → LLM Provider (
/settings/provider) — mode toggle + BYOK form. Credential dropdown comes from your active workspace vault; if it's empty, the form points you at/credentialsfirst. Save and the page revalidates with the resolved config. - Settings → Billing (
/settings/billing) — read-only summary dashboard. Headline balance + disabled "Purchase Credits" button (tooltip: "Coming in v2.1"); Usage tab grouped by event; Invoices and Payment Method tabs render empty states for the v2.1 cutover.
- CLI: drop any direct calls to the workspace
/credentials/llmroute. Store your provider credential in the workspace vault (zombiectl credential add ...), thenzombiectl tenant provider set --credential <name>. Verify withzombiectl tenant provider getand run a test event. - Dashboard: existing tenants stay on platform-managed by default; nothing breaks. To switch to BYOK, head to Settings → LLM Provider in Mission Control.
- Custom integrations consuming the public model-caps endpoint can now read
input_cents_per_mtok/output_cents_per_mtokper model. The shape is additive — old fields still present.
- Pricing visibility. Per-model rates are now in the public-but-unguessable
model-caps.jsonresponse. Anyone who finds the cryptic URL can read platform margins. We accept the trade-off — it preserves the cacheable, unauthenticated property that letstenant provider setresolve at low latency without a tenant token. We'll revisit if a competitor uses the data strategically. - No plan tiers. "Free" is not a tier — it's just "the user hasn't exhausted the $10 starter grant yet." Both platform and BYOK postures run through the same
processEventandcompute_*_chargefunctions; they differ in drain rate, not in eligibility.
<Update label="May 1, 2026" tags={["Breaking", "API"]}>
Two REST endpoints lose their verb-shaped URLs in favor of resource collections, and the in-process router moves to a segment-based matcher under the hood. Pre-v1.0 carve-out applies — old URLs return 404, not 410, and there is no compatibility shim.
Two URL renames. CLI and server should upgrade together.
-
Steering a zombie.
POST /v1/workspaces/{ws}/zombies/{zid}/steer→POST /v1/workspaces/{ws}/zombies/{zid}/messages. Request body shape is unchanged. The CLI subcommand stayszombiectl zombie steer— verb on the CLI, noun on the wire. -
Memory tools. Four verb endpoints collapse into one resource:
POST /v1/memory/store→POST /v1/workspaces/{ws}/zombies/{zid}/memoriesGET /v1/memory/recall?...→GET /v1/workspaces/{ws}/zombies/{zid}/memories?query=...GET /v1/memory/list?...→GET /v1/workspaces/{ws}/zombies/{zid}/memories(omit?query=)POST /v1/memory/forget→DELETE /v1/workspaces/{ws}/zombies/{zid}/memories/{memory_key}
DELETEis now idempotent — a missing key returns204 No Contentwith an empty body. The previous{"deleted": true|false}response is gone.zombie_idis a path segment everywhere — drop it from the query string. Thememory_store/memory_recallagent-tool names are unchanged; this is the HTTP surface only.
- Stricter routing. The dispatcher now parses each request path into segments once at the boundary, so
//and trailing slashes no longer silently match wrong handlers. Malformed paths return404deterministically. - Single source of truth for
v1. The version literal lives in exactly one place in the router. Adding a futurev2is a one-line change.
POST /v1/workspaces/{workspace_id}/zombies/{zombie_id}/messages— same body and response as the retired/steer.GET /v1/workspaces/{workspace_id}/zombies/{zombie_id}/memories?query=&category=&limit=— list-or-search collection. Presence of?query=flips behavior from list-most-recent to fuzzy search across key and content.POST /v1/workspaces/{workspace_id}/zombies/{zombie_id}/memories— store a single entry.DELETE /v1/workspaces/{workspace_id}/zombies/{zombie_id}/memories/{memory_key}— idempotent204.
Retired URLs (/v1/workspaces/{workspace_id}/zombies/{zombie_id}/steer, /v1/memory/{store,recall,list,forget}) return 404 with no body.
<Update label="Apr 30, 2026" tags={["Breaking", "API", "CLI"]}>
Two legacy verb-suffix endpoints retire in favor of PATCH on the underlying resource. Completing an auth session and killing a zombie now ride a single PATCH per resource — closer to standard REST semantics, easier to discover from OpenAPI, and consistent with how every other workspace and zombie field already behaves. Alongside the rename, zombie config edits now hot-reload mid-loop: an operator updating core.zombies.config_json from Mission Control sees the new tools, network policy, and context budget take effect on the running worker without restarting the zombie thread. The control-stream signal that already existed for kills now also carries config-revision changes; the worker reparses, swaps the in-memory config, and frees the old allocation between events.
Every CLI/SDK call against the retired URLs needs an update. The CLI commands themselves (zombiectl kill, zombiectl login) are unchanged — they always wrapped these URLs internally. Direct API consumers must migrate:
POST /v1/workspaces/{ws}/zombies/{zombie_id}/kill→PATCH /v1/workspaces/{ws}/zombies/{zombie_id}with body{ "status": "killed" }. Same auth, same response shape. Re-killing a killed zombie still returns 404 (idempotent-fail).POST /v1/auth/sessions/{session_id}/complete→PATCH /v1/auth/sessions/{session_id}with body{ "status": "complete", "token": "<user-jwt>" }. The response now mirrors the GET poll shape ({ status, token, request_id }).POST /v1/workspaces/{ws}/zombies/{zombie_id}/steeris unchanged in this release. The steer rename toPOST /eventswith a polymorphic body is scheduled for the next URL hygiene pass.
Both retired URLs return 404 — no 410 stub. CLI and server may be upgraded independently; the CLI was already issuing the new shapes before this release and nothing else in zombiectl needs to change.
- Config hot-reload mid-loop. Edit a zombie's config in Mission Control (or via
PATCH /v1/.../zombies/{id}withconfig_json) and the running worker observes the new revision between events. Tools list, network allowlist, secrets map, and the three context-budget knobs (tool_window,memory_checkpoint_every,stage_chunk_threshold) all swap on the next event boundary. The old config is freed in the same step — no memory leaks on config swap. - One PATCH for combined updates.
PATCH /v1/.../zombies/{id}accepts{ config_json, status }together. Setting both in one request issues one SQL update and one control-stream signal per dirty surface, so a config-and-kill in one request stays atomic. - Cleaner OpenAPI surface. The bundled spec at
/openapi.jsonshed three verb-suffix paths and three pending-rename carve-outs. Slack and GitHub OAuth callbacks moved to a separate vendor-immortal classification — they're still pinned, but the API hygiene gate now distinguishes external contracts from internal cleanup debt.
Updated routes (the substantive shape changes):
PATCH /v1/workspaces/{workspace_id}/zombies/{zombie_id}— body is partial:{ config_json?, status? }. Both fields optional; an empty body is a 200 no-op. Whenstatusis set it must equal"killed". Response:{ zombie_id, status?, config_revision }. Thestatusfield is present only when the request set it.PATCH /v1/auth/sessions/{session_id}— body:{ status: "complete", token }. Bearer auth (the depositor proves it can mint a user-jwt). Response mirrors the GET poll shape:{ status, token, request_id }.
Retired routes (404 in this release, no 410 stub):
POST /v1/workspaces/{ws}/zombies/{zombie_id}/killPOST /v1/auth/sessions/{session_id}/complete
No new error codes. The validation message for invalid status values is status must be "killed" (returned with UZ-VAL-001).
No surface change. zombiectl kill <zombie_id> and zombiectl login issue the new URLs internally — anyone scripting against the CLI sees the same exit codes and JSON shapes as before.
<Update label="Apr 29, 2026" tags={["Breaking", "API", "CLI"]}>
TRIGGER.md frontmatter no longer carries runtime keys at the top level. tools, credentials, network, budget, and trigger now live under a single x-usezombie: block. Top-level stays minimal — just name: for cross-file identity. SKILL.md gains validated authoring metadata: name, description, and version are required at the top level; tags, author, model, and when_to_use pass through. Install rejects any bundle whose SKILL.md name: does not match TRIGGER.md name:.
Every existing zombie bundle needs both files updated. The migration is mechanical:
- TRIGGER.md — add
x-usezombie:at the top level and indent your existingtrigger:,tools:,credentials:,network:, andbudget:blocks under it. Keepname:at the top. - SKILL.md — ensure the frontmatter has
name:,description:, andversion:. Makename:exactly match the value in TRIGGER.md. zombiectl install --from <dir>parses both files and reports field-level errors. Re-run until clean.
See Authoring skills for the canonical shape and a working platform-ops-zombie example.
- Disciplined parser. Unknown subkeys under
x-usezombie:fail loud (UnknownRuntimeKey) so typos surface at install instead of degrading silently. Top-level keys stay permissive — drop inx-amp:or other vendor blocks without breaking install. - Cross-file identity.
name:must match across SKILL.md and TRIGGER.md. One identity per zombie bundle, enforced at install. - Real YAML. The bespoke YAML→JSON converter is replaced with
kubkon/zig-yaml0.2.0. Multi-line strings, escapes, the standard scalar tags, and arbitrary nesting depth all work as you'd expect from any YAML 1.2 tool.
Two new error codes from POST /v1/workspaces/{ws}/zombies:
UZ-ZMB-008—MSG_ZOMBIE_INVALID_CONFIGnow also fires when SKILL.md frontmatter is malformed or missing required fields.UZ-ZMB-011—MSG_ZOMBIE_NAME_MISMATCH. Returned when SKILL.mdname:and TRIGGER.mdname:disagree.
Internal SQL paths into core.zombies.config_json move from config_json->'trigger'->... to config_json->'x-usezombie'->'trigger'->.... No external surface change; mentioned for operators reading raw rows.
<Update label="Apr 29, 2026" tags={["What's new", "API", "UI"]}>
Approval gates used to flow only through Slack DMs. Operators looking at Mission Control saw a "healthy" zombie even when it was stalled at a gate. The inbox closes that loop. Every pending gate now surfaces in a workspace-wide /approvals list and on each zombie's detail page, with the proposed action, blast-radius assessment, evidence, and a timeout countdown rendered next to Approve and Deny buttons. Resolutions go through a single channel-agnostic core shared by Slack and Mission Control, so a click in either place makes the other channel's stale button no-op cleanly with the original outcome and resolver attribution. A background sweeper auto-denies any pending gate whose 24-hour timeout has elapsed, attributing the resolution to system:timeout so operators can tell auto-denials apart from manual ones.
/approvalspage. Workspace-wide list of pending gates, sorted oldest-first (oldest is most urgent). Each row shows the zombie name, gate kind badge, proposed-action one-liner, blast-radius callout, age, and timeout countdown, with inline Approve and Deny buttons. The list refreshes every 5 seconds; resolutions remove the row optimistically. Empty workspaces render a clean "No pending approvals" state./approvals/{gate_id}detail page. Full proposed-action prose, evidence rendered as expandable JSON, blast-radius callout, key/value context grid (zombie, tool, action, kind, requested-at, auto-deny-at, action id), and a Resolve panel with an optional reason textarea. Once resolved, the page flips to a Resolution panel showingResolved as <outcome> by <who> at <when>.- Per-zombie Pending approvals section. The zombie detail page gains a "Pending approvals" panel filtered to that zombie, plus a destructive-variant badge in the page header showing
N pending approval(s)(or50+past the page-size). - Sidebar nav. New "Approvals" entry between Credentials and Events.
- Slack and Mission Control parity. Slack callbacks and Mission Control clicks now share one resolve core. The schema-level append-only trigger plus the
WHERE status='pending'precondition give at-most-one-resolution guarantees — the loser sees 409 with the original outcome and resolver attribution, never silent overwrite. - Auto-timeout sweeper. A background thread on the API process scans
core.zombie_approval_gatesevery 60 seconds for pending rows whosetimeout_athas passed, and transitions them totimed_outvia the same resolve core. Worker treatstimed_outasdeniedfor safety on destructive operations. Default timeout is 24 hours.
GET /v1/workspaces/{ws}/approvals?status=&zombie_id=&gate_kind=&cursor=&limit=— paginated list. Defaultstatus=pending,limit=50, max 200. Cursor encodes(requested_at, gate_id)so concurrent inserts don't cause silent skips. Response shape:{ items: ApprovalGate[], next_cursor: string|null }. Filterable byzombie_idandgate_kind.GET /v1/workspaces/{ws}/approvals/{gate_id}— single-row read. 404 when the gate doesn't exist OR belongs to a different workspace (no information leak).POST /v1/workspaces/{ws}/approvals/{gate_id}:approve— body{reason?: string ≤ 4096}. 200 with{gate_id, action_id, outcome: "approved", resolved_at, resolved_by}. 409UZ-APPROVAL-006with the same shape when another channel got there first; the body'soutcomeandresolved_byreflect the original resolver. 404 on unknown gate id (including cross-workspace).POST /v1/workspaces/{ws}/approvals/{gate_id}:deny— same shape.outcomeisdeniedon success.ApprovalGateshape includes the new operator-visible fields (gate_kind,proposed_action,evidenceas JSONB,blast_radius,timeout_at,resolved_by) on top of the existing audit fields.
- Slack and Mission Control race no longer overwrites silently. Before this release, the Slack callback wrote the Redis decision key directly without a DB precondition; a Mission Control click that happened to land first could be overwritten by the Slack click moments later. Both paths now go through the same DB UPDATE WHERE status='pending' atomic transition; the loser observes 409 with the original outcome.
<Update label="Apr 29, 2026" tags={["Internal", "Performance"]}>
Internal performance pass on the worker → live-tail pipe surfaced by the Apr 28, 2026 streaming substrate review. JSON encoding for activity frames now reuses a per-event scratch buffer, eliminating the per-frame heap alloc on chunk-heavy responses (chunk-encode benchmark drops from ~43µs to ~2µs). The executor transport parses each progress frame once instead of twice (~46% faster). Each worker now opens a dedicated Redis client for activity PUBLISH so the per-frame publish no longer contends with stream commands on the queue client's mutex. The per-zombie events index now leads with (zombie_id, created_at DESC, event_id DESC) — covers the dashboard's primary view and keyset cursor pagination directly, where the prior actor-prefixed index forced a sort-and-scan. No user-visible behavior change; operators may notice steadier live-tail latency once concurrent dashboard tabs grow.
<Update label="Apr 28, 2026" tags={["Breaking", "What's new", "API", "CLI", "UI"]}>
Streaming substrate: every event has provenance, operators can steer, and live activity tails the dashboard
Three things converge in this release. First, every event a zombie processes — operator steer, GitHub webhook, scheduled cron, chunked continuation for long responses, gate-resolved continuation — now lands on a single Redis stream with a normalized envelope and an actor field that carries provenance forward. Second, every event start and end is durably persisted in core.zombie_events with the request payload, the response, token count, wall time, and failure label, queryable through a new history endpoint with cursor pagination, actor glob filters, and a humanized since= parameter. Third, the dashboard now ships a live activity panel that streams tool calls, response chunks, and completion frames over Server-Sent Events with a sub-200 ms publish-to-receive budget.
Operators get two new CLI subcommands. zombiectl steer {id} "<message>" POSTs to the new ingress, opens the SSE stream, and prints [claw] chunks as they arrive — Ctrl-C closes the watcher without killing the zombie. zombiectl events {id} paginates the history with --actor=, --since=, --json, and --cursor= filters.
POST /steerbody and response shape changed. The endpoint now does a directXADDon the per-zombie event stream and returns{event_id}so callers can correlate. The previous SET/GETDEL key-poll path is gone; the legacyzombie:{id}:steerRedis key is no longer touched. If you have a script reading the steer key directly to detect inflight steers, switch to either the SSE stream or the events history endpoint.GET /v1/.../zombies/{id}/activityis removed. Both the per-zombie variant and the workspace-aggregate variant. Replace per-zombie activity reads withGET /v1/workspaces/{ws}/zombies/{id}/events. Replace workspace-aggregate reads withGET /v1/workspaces/{ws}/events?zombie_id={id}for the drill-down or omitzombie_idfor the workspace-wide feed. Both responses now carryactor,status,response_text,tokens, andwall_msinstead of the oldevent_type/detailshape.zombiectl logsautomatically uses the new endpoint; if you have direct API consumers, switch the URL and update the row parser.core.activity_eventstable is dropped. Pre-v2.0 teardown — no migration. Anything that read this table directly will break; switch tocore.zombie_events. The new table's primary key is composite(zombie_id, event_id)to support idempotent replay under XAUTOCLAIM redelivery.- Executor RPC framing version bumped to v2. Worker and executor binaries must upgrade together — they perform a HELLO handshake on connect and abort with
executor.rpc_version_mismatchon a mismatch. Roll the executor first, then the worker.
- One ingress, one durable record per event. Every event landing on
zombie:{id}:eventsproduces exactly one new row incore.zombie_events(mutable, lifecycle-tracked), one new row inzombie_execution_telemetry(immutable billing audit), and one mutated row incore.zombie_sessions. All three reference the sameevent_id, so a single join key threads narrative, billing, and session state. Replays are idempotent viaON CONFLICT DO NOTHINGon the composite key plus a unique constraint on telemetry. - Continuation actors stay flat with origin tags. When chunking splits a long response or a blocked gate is resolved, the new event re-enters the stream with
actor=continuation:<original_actor>— nevercontinuation:continuation:...no matter how deep the chain. A singleactor LIKE '%steer:kishore'filter finds the origin and every continuation in one pass. Each continuation'sresumes_event_idpoints at its immediate parent, so a recursive CTE walks the chain back to its origin. gate_blockedevents are visible but unresolvable until the Approval Inbox ships. The row enters terminal state withstatus='gate_blocked',failure_labelpopulated, and an XACK so the worker doesn't redeliver. Operators can see stranded events viaGET /events?actor=.... The admin-resume fallback was deliberately dropped from this release; resolution is owned by the upcoming Approval Inbox.- Dashboard live panel.
/zombies/{id}renders the new<LiveEventsPanel />above the event history table. NativeEventSourceconnects to a same-origin Next Route Handler that mints an API-audience JWT server-side and proxies the upstream stream — the browser never holds the JWT, the backend never sees a cookie. Reconnects with exponential backoff capped at 15 s; rolling buffer of the last 20 frames.
POST /v1/workspaces/{ws}/zombies/{id}/steer— body{message: string (≤8192 chars)}. 202 with{status: "accepted", event_id: string}. Theevent_idis the Redis stream entry id; CLIs and dashboards correlate the SSE feed to this id.GET /v1/workspaces/{ws}/zombies/{id}/events?cursor=&actor=&since=&limit=— paginated history.actoraccepts globs (steer:*,webhook:*) and exact matches (webhook:github).sinceaccepts Go-style durations (15s,30m,2h,7d) or RFC 3339 timestamps (2026-04-25T08:00:00Z). Defaultlimit=50, max 200.sinceandcursorare mutually exclusive — supplying both returns 400.GET /v1/workspaces/{ws}/events?cursor=&actor=&zombie_id=&since=&limit=— workspace-aggregate history. Same parameter shape as the per-zombie variant; items carry an extrazombie_idso the workspace overview can group by zombie. Replaces the deleted/activityendpoint.GET /v1/workspaces/{ws}/zombies/{id}/events/stream— SSE live tail.Content-Type: text/event-stream. Frame kinds:event_received,tool_call_started,tool_call_progress(~2s heartbeat for long tool calls),chunk,tool_call_completed,event_complete. Per-connection sequence ids reset to 0 on every new SUBSCRIBE; the server ignores theLast-Event-IDrequest header. After a disconnect, clients backfill viaGET /events?since=<last_seen>then reopen the stream.
zombiectl steer {id} "<message>"— batch mode. POSTs the message, opens the SSE stream, filters frames on the returnedevent_id, prints[claw] <chunk>as response chunks arrive, exits 0 onevent_completewithstatus=processedand non-zero onagent_error. Falls back to pollingGET /events?since=<event_id>if the SSE drops, with a 60-second deadline. Interactive REPL mode (no message argument) is deferred to a follow-up release; callingsteer {id}without a message currently exits 2 with a helpful pointer.zombiectl events {id}— paginated history print.--actor=steer,--actor=webhook:github,--since=2h,--since=2026-04-25T08:00:00Z,--json(raw records for piping),--cursor=<token>(resume from a previous page). Default 50 events per page; the next-cursor hint prints below the last row when more results exist.zombiectl logs {id}— repointed at the new events endpoint (the activity stream is gone). Same flag shape; row format now showsactor+response_textsummary instead ofevent_type+detail.
<Update label="Apr 27, 2026" tags={["Bug fixes", "API", "CLI", "Security"]}>
Install actually works now: contract aligned, parser key matches the sample, doctor preflight tightened
Three small bugs were stacking up to make zombiectl install --from <path> impossible to use against a fresh workspace. The CLI was sending one shape; the API expected another. The shipped sample uses tools: in TRIGGER.md; the parser was looking for skills:. And both install and doctor were exempt from the local auth guard, so missing credentials surfaced as a confusing 401 from the server instead of a clean local "log in first" message. All three are fixed in one pass.
- Install POST shape changed.
POST /v1/workspaces/{ws}/zombiesnow accepts{trigger_markdown, source_markdown}. The previous{name, config_json, source_markdown}shape is gone. The server is the single parser of TRIGGER.md frontmatter —nameand the persistedconfig_jsonare derived server-side from the YAML between the---fences. If you have a script that POSTs directly to this endpoint, switch to sending the raw two markdown files. Pre-v1.0; no compat shim. - TRIGGER.md key renamed
skills:→tools:. The shipped sample (samples/platform-ops/TRIGGER.md) already usedtools:; the parser now matches. If you have an older zombie spec with a top-levelskills:array, rename that key totools:before installing. The server returnsERR_ZOMBIE_INVALID_CONFIGwith a hint when the canonical key is missing. zombiectl installandzombiectl doctornow requirezombiectl loginfirst. Previously they were exempt from the auth guard and produced opaque 401s on missing credentials. Now they fail locally withAUTH_REQUIREDbefore any HTTP call. Onlyloginitself is exempt.
- Doctor reports the three things that actually matter. The new check set is
server_reachable(GET /healthz with a 5s timeout),workspace_selected(local config has a current workspace), andworkspace_binding_valid(your token is bound to that workspace, verified by a 200 from the workspace-scoped zombies list). Previoushealthz/readyz/credentials/workspacechecks are folded in or dropped — credentials is now covered by the auth guard, readyz overlaps healthz. - Doctor
--jsonreturns a stable schema.{ok: bool, api_url: string, checks: [{name, ok, detail}]}. Skills and scripts can consume it without grep on prose. Each failed check carries a one-linedetailpointing at the next concrete action. - Install response carries the canonical name.
POST /zombiesnow returns{zombie_id, name, status}. The CLI displays the server-derived name instead of guessing from the directory basename — copy/paste names match what the server stored.
POST /v1/workspaces/{workspace_id}/zombies— body{trigger_markdown: string (≤64KB), source_markdown: string (≤64KB)}. 201 with{zombie_id, name, status}. 400ERR_ZOMBIE_INVALID_CONFIGif the frontmatter parse fails (missingname:, missingtools:, missing---fences, etc.). 400ERR_INVALID_REQUESTwithMSG_ZOMBIE_TRIGGER_REQUIREDiftrigger_markdownis empty or oversized.
zombiectl install --from <path>— POSTs{trigger_markdown, source_markdown}(the previous{source_markdown, trigger_markdown}shape was rejected by the API). The display name in🎉 <name> is live.comes from the server response, falling back to the directory basename only if the server omits it.zombiectl doctor— three checks (server_reachable,workspace_selected,workspace_binding_valid). Per-check 5s timeout, exit 0 on all-green and exit 1 on any failure. Now requires authentication; runzombiectl loginfirst.
<Update label="Apr 26, 2026" tags={["What's new", "API", "CLI"]}>
Zombies installed via POST /v1/workspaces/{ws}/zombies are now claimed by a worker thread within ~1s of the 201 — no worker restart needed. A new POST /v1/workspaces/{ws}/zombies/{id}/kill aborts an in-flight zombie cleanly and propagates the cancel to the executor, replacing the legacy DELETE /…/zombies/{id} shortcut. A new PATCH /v1/workspaces/{ws}/zombies/{id} updates a zombie's config and signals the worker to pick up the new revision on its next loop iteration. SIGTERM on the worker now triggers a graceful drain instead of cutting in-flight events mid-call.
- Atomic install path. The create endpoint now does INSERT into
core.zombies+XGROUP CREATE MKSTREAM zombie:{id}:events+XADD zombie:control * type=zombie_createdsynchronously before returning 201. By the time the API responds, the per-zombie data stream and the control-plane signal both exist; a webhook arriving 1ms after the 201 finds the consumer group already. - Fleet-wide control plane. A new Redis stream
zombie:controlcarries lifecycle signals (created / status_changed / config_changed / drain_request). One watcher thread per worker process consumes it viaXREADGROUPand dispatches to spawn / cancel / reconfigure handlers — no more "zombie installed at 14:00 invisible to the worker that started at 13:00" until the next restart. - Per-zombie cancel flag. Each zombie thread observes a per-zombie atomic flag at the top of every loop iteration.
POST /killflips the flag and the thread exits within ~100ms, regardless of where it was in its event loop. zombiectl kill <zombie_id>now POSTs to/killand requires an explicit zombie id (was previously a DELETE that defaulted to "kill all in workspace" when no id was passed — that footgun is gone).
POST /v1/workspaces/{workspace_id}/zombies/{zombie_id}/kill— 200 with{zombie_id, status: "killed", queued_at}. 404 if the zombie does not exist or is already killed (idempotent semantics fold into 404).PATCH /v1/workspaces/{workspace_id}/zombies/{zombie_id}— body{config_json?: string}. 200 with{zombie_id, config_revision}whereconfig_revisionis the newupdated_attimestamp (strictly monotonic per zombie).DELETE /v1/workspaces/{workspace_id}/zombies/{zombie_id}— removed.POST /killreplaces it with a clean verb.
zombiectl kill <zombie_id>— POST to the new/killendpoint. Argument is now required.
<Update label="Apr 24, 2026" tags={["What's new", "Integrations"]}>
A new zombie lives at samples/platform-ops/. It wakes on a GitHub Actions workflow_run.conclusion=failure webhook, gathers evidence from the failed workflow's logs, your hosting provider, and your data-plane, then posts an evidenced diagnosis to a Slack channel. Same zombie is reachable manually via zombiectl steer {id} for a morning health check or any operator-driven investigation. Read-only against GitHub, Fly, and Upstash; its one write path is the Slack post. Credentials are structured {host, api_token} / {host, bot_token} records in the workspace vault; raw token bytes are substituted into outbound HTTPS requests at the credential firewall, after the executor sandbox closes around the agent — they never reach the LLM context, logs, or database.
samples/platform-ops/ships withSKILL.md(diagnosis prompt, evidence-gathering flow, budget prose ≤ $8/month,http_requestas the primary tool,cron_addgated on the operator asking for recurring polling),TRIGGER.md(trigger.type: webhookfor GitHub Actions plus manual steer, the built-in tools actually used, network allowlist forapi.github.com/api.fly.io/api.upstash.com/slack.com, $1/day + $8/month budget caps), and aREADME.mdoperator walkthrough covering install, wiring the GitHub Actions webhook, chatting, an example diagnosis, and credential hygiene.- Four credential shapes land alongside:
github = {host, api_token},fly = {host, api_token},upstash = {host, api_token},slack = {host, bot_token}. Add them viazombiectl credential add <name> --host <host> --api-token <token>(use--bot-tokenforslack). - Install works via
zombiectl install --from samples/platform-ops. The webhook URL printed at install time is the one to paste into your GitHub repo's webhook settings (filter toworkflow_run). - Sandbox: bwrap + landlock + cgroups on Linux; the agent runs in a locked-down process with network deny-by-default and only the
network.allowhosts reachable. - Every event lands in
core.zombie_eventswithactor=webhook:github(deploy-failure firing) oractor=steer:<operator>(manual investigation), so the timeline reads cleanly regardless of who poked the zombie.
<Update label="Apr 22, 2026" tags={["What's new", "Breaking", "UI", "API", "CLI"]}>
Mission Control reaches its first "I can run my day from here" shape. Sign in to app.usezombie.com and you get an overview page with live status tiles + recent activity, a zombies list with cursor pagination and in-view search, an install form, and a per-zombie detail page that shows the webhook URL, the full config, and a one-click kill switch. Firewall, credentials, and settings pages are in place as placeholders; they'll fill in as the underlying features ship.
Multi-workspace operators get a workspace switcher in the header. Selecting a workspace persists the choice in a cookie and revalidates the current page — no sign-out or token reissue required.
We also cleaned up the kill-switch endpoint while we were in the neighborhood: the zombie routes no longer carry action verbs in their paths. Any caller hitting the legacy kill endpoint must migrate; details under Upgrading.
Credit exhaustion is now operator-visible in Mission Control. When a tenant's balance hits zero, a destructive banner appears above the zombies list and a "Balance exhausted" badge renders on each zombie's detail page — both driven by the is_exhausted / exhausted_at fields on GET /v1/tenants/me/billing.
- Kill switch moved from
POSTtoDELETEon a new path. Replace any caller hittingPOST /v1/workspaces/{ws}/zombies/{id}/stopwithDELETE /v1/workspaces/{ws}/zombies/{id}/current-run. Same behavior, same response shape, same 200 / 409 / 404 semantics. Thezombie_stoppedactivity event is unchanged. The old path now returns 404 — pre-1.0 alpha breakage, no deprecation window. If you have dashboards, runbooks, or scripts pointing at.../stop, update them. - The rename is a REST-hygiene change: "current-run" is a singleton sub-resource of the zombie, and DELETE is the idiomatic verb for "kill the running action." It also unblocks a symmetric
GET /current-runin a future release for run-state queries without reintroducing action verbs in paths.
- Overview dashboard at
/— status tiles for active / paused / stopped zombies and the tenant credit balance, plus a live "Recent Activity" feed. Renders as a Server Component with independent Suspense boundaries so a slow endpoint doesn't block first paint. - Zombies list at
/zombies— cursor pagination with a "Load more" button and in-view search across name, id, and status. Built onGET /v1/workspaces/{ws}/zombies?cursor={ts}:{id}&limit=N(see API reference). - Install form at
/zombies/new— validates required fields client-side, migrated onto the design-systemFormprimitive (react-hook-form + zod). Surfaces a clear toast when a name already exists. - Zombie detail at
/zombies/[id]— webhook URL with one-click copy, trigger panel, firewall-rules panel, zombie config (rename / describe / delete-with-confirm), and a React-19useOptimistic-powered kill switch with 409 auto-recovery. - Workspace switcher in the header — backed by a new
GET /v1/tenants/me/workspacesendpoint (see API reference) plus a Server Action that writes theactive_workspace_idcookie and revalidates. Works without re-issuing your session. - Placeholder pages at
/firewall,/credentials,/settingsso the sidebar shows the full shape of what's coming. - Credit-exhaustion banner + per-zombie badge wired to
GET /v1/tenants/me/billing— no configuration needed, appears automatically when a tenant runs out. - Auth abstraction: every
@clerk/nextjscall now flows throughlib/auth/server.tsandlib/auth/client.ts. Switching auth provider in a future release is a two-file edit. - Same-origin
/backendproxy: browser-side fetches go through/backend/:path*which the Next config rewrites toAPI_BACKEND_URL. No more CORS surprises in dev, preview, or prod. - Animated loading states on every async action (install, delete, route navigation, workspace switch).
New: GET /v1/tenants/me/workspaces — returns every workspace the caller's tenant owns. Backs the workspace switcher.
{
"items": [
{ "id": "ws_01HW...", "name": "Production", "created_at": 1713700000000 },
{ "id": "ws_01HX...", "name": "Staging", "created_at": 1713700000001 }
],
"total": 2
}Changed: GET /v1/workspaces/{workspace_id}/zombies now accepts ?cursor={timestamp}:{id}&limit=N (default 20, max 100). Response gains a nullable cursor field holding the key for the next page (null at the end). Unpaginated callers keep working — absent cursor / limit yields the first 20.
GET /v1/workspaces/ws_01HW.../zombies?limit=2
{
"items": [ { "id": "zom_01...", "name": "alpha", "status": "active" }, ... ],
"total": 2,
"cursor": "1713700050000:zom_01..."
}Renamed (breaking):
DELETE /v1/workspaces/{workspace_id}/zombies/{zombie_id}/current-runTransitions the zombie's status from active or paused to stopped and records a zombie_stopped activity event. Returns 200 {zombie_id, workspace_id, status: "stopped", request_id}. Returns 409 UZ-ZMB-010 if already stopped or killed, 404 UZ-ZMB-009 if the zombie is not in the path workspace. Requires the operator role.
zombiectl --helpnow surfaces the full zombie-lifecycle commands —install | up | status | kill | logs | credential— alongside the existinglogin / workspace / specs / doctorcommands.zombiectl list [--workspace-id ID] [--cursor C] [--limit N] [--json]— new subcommand that mirrors the/zombieslist in Mission Control (cursor-paginated, honours the samelimit ≤ 100clamp).zombiectl workspace show [--workspace-id ID]— new subcommand that mirrors the Mission Control/settingspage (prints workspace ID, name, and active-workspace status).- Active workspace is now persistent.
zombiectl workspace use <workspace_id>writes it to~/.config/zombiectl/workspaces.json; subsequent commands (zombiectl list,zombiectl status,workspace show, etc.) default to it when--workspace-idis omitted. Mission Control'sactive_workspace_idcookie and the CLI's config file stay independent — setting one doesn't affect the other. zombiectl upstill prints the🎉 Woohoo! Your zombie is installed and ready to run.success line (unchanged).zombiectl killis unaffected by the kill-switch path rename — it continues to callDELETE /zombies/{id}(full delete, not the current-run kill) as it always did.
<Update label="Apr 22, 2026" tags={["What's new", "Breaking", "API", "Security", "Billing"]}>
The env-var API_KEY bypass that minted an admin principal with no tenant and no audit identity has been removed. Admin authentication now flows exclusively through Clerk sessions with publicMetadata.role=admin — set once per operator in the Clerk Dashboard, revoked instantly from the same place, and carried into every request JWT. Programmatic admin access uses a tenant-minted zmb_t_… key from POST /v1/api-keys (shipped in v0.26.0). Separately, the tenant billing response now surfaces credit exhaustion, and a new policy knob lets operators decide what the worker does when a tenant hits zero.
- Remove
API_KEYfrom your server environment. If your deployment still passesAPI_KEY, it is now ignored. The server refuses to start without OIDC (OIDC_JWKS_URL,OIDC_ISSUER,OIDC_AUDIENCE) — no fallback. - Promote your admin user in Clerk. Dashboard → Users → select user → Metadata → Public metadata → set
{"role": "admin"}. The operator playbook atplaybooks/012_usezombie_admin_bootstrap/001_playbook.mdwalks through dev + prod step-by-step and ends by minting azmb_t_…key for CI / scripts and stowing it atop://ZMB_CD_<env>/usezombie-admin/api_key. - If you consumed the
balance_cents == 0branch, switch to readingis_exhausted/exhausted_atonGET /v1/tenants/me/billing(see below).
BALANCE_EXHAUSTED_POLICY={continue|warn|stop}(defaultwarn).stoppre-empts delivery for an exhausted tenant — the zombie never runs, Redis gets an XACK so the event doesn't retry, and abalance_gate_blockedactivity event is recorded.warnlogs and emits a rate-limitedbalance_exhaustedactivity event (1 per workspace per 24h).continueis the old "log and let it run free" behavior, made explicit.- First-exhausting debit stamps
balance_exhausted_atatomically and writes a one-shotbalance_exhausted_first_debitactivity event. Replays do not double-emit.
GET /v1/tenants/me/billing gains two fields on every response:
is_exhausted—boolean, true once the tenant's balance has hit zero on a worker debit.exhausted_at—integer(epoch ms) ornull. Non-null only onceis_exhaustedis true.
The OpenAPI schema lists both as required with exhausted_at nullable.
<Update label="Apr 22, 2026" tags={["Bug fixes", "What's new", "Observability"]}>
Two observability paths that looked live but weren't are now actually live. The per-workspace Prometheus token counter is now emitted on every successful zombie delivery, and a new zombie_id label lets you slice the same counter by zombie. The OTLP JSON exporter now forwards histogram data points (_bucket, _sum, _count) instead of silently dropping them.
- Prometheus counter
zombie_agent_tokens_by_workspace_totalnow carries bothworkspace_idandzombie_idlabels and reports real data after each completed delivery. Useful for top-N spend dashboards at either granularity. zombie_workspace_metrics_overflow_totalis exposed so operators can detect when the fixed-capacity slot table (4096(workspace_id, zombie_id)pairs) saturates and falls back to an_otheraggregation bucket.
- Per-workspace token counter was a no-op: the helper existed but no production code path called it. It now fires from the same spot that records
zombie_tokens_total, so Grafana queries against the per-workspace family return real values instead of zero. - OTLP JSON exporter silently dropped
_bucket/_sum/_countlines, so histograms (zombie_execution_seconds,zombie_agent_duration_seconds,zombie_executor_agent_duration_seconds) never reached an OTLP collector. The exporter now emits OTLP histogram data points with cumulative-to-delta bucket conversion,explicitBounds, andaggregationTemporality: 2(CUMULATIVE). - Removed the
zombie_gate_repair_loops_by_workspace_totalandzombie_gate_repair_loops_totalcounters (plus their helpers) — gate-repair is a pipeline-era concept with no zombie-era call site, so these counters always read zero and misled operators into expecting data that could never appear.
<Update label="Apr 22, 2026" tags={["What's new", "UI", "CLI", "Integrations"]}>
The docs.usezombie.com site has been rewritten end-to-end against the current product. The new quickstart walks a fresh operator from Clerk sign-up to a live zombie firing webhook events in under ten minutes, against the shared $10 tenant balance introduced earlier this month. Stale pre-Clerk vocabulary — redemption flows, legacy "lead-collector"-centric examples — has been cleared from every page outside the historical changelog entries that predate this release.
- New quickstart. Sign up → dashboard → create zombie → copy webhook URL →
curltrigger → verify the credit debit in the billing UI. End-to-end in one page. - New CLI reference at
/cli/zombiectl— everyzombiectlcommand with copyable examples. - Self-hosting section under
/operator— deployment architecture, configuration, security, observability, and operations pages for running the control plane yourself. - Concepts page updated to cover the four nouns (tenant, workspace, zombie, skill) and the tenant-scoped credit model.
- Billing pages rewritten around the single-wallet, multi-workspace model.
No binary changes in this release — the docs now accurately describe the shipped CLI surface (zombiectl install | up | status | logs | kill | credential). Event triggering is documented as curl against the webhook URL, not a CLI subcommand.
<Update label="Apr 21, 2026" tags={["What's new", "API", "Billing"]}>
Billing now lives at the tenant, not the workspace. Every new signup gets exactly one billing.tenant_billing row at plan_tier=free, plan_sku=free_default, and a 1000¢ free-credit balance. Any zombie run in any workspace owned by that tenant debits the same shared balance — creating a second workspace no longer grants additional credits, and plan changes no longer have to fan out across workspace rows. Per-workspace credit state and the workspace-scoped billing lifecycle endpoints are removed.
POST /v1/workspaces/{workspace_id}/billing/eventsPOST /v1/workspaces/{workspace_id}/billing/scaleGET /v1/workspaces/{workspace_id}/billing/summaryGET /v1/workspaces/{workspace_id}/zombies/{zombie_id}/billing/summaryPOST /v1/workspaces/{workspace_id}/scoring/config
- One tenant, one billing row:
billing.tenant_billingholds(plan_tier, plan_sku, balance_cents, grant_source, updated_at)with the tenant id as the primary key. - Worker debits the tenant balance atomically on every completed run via a conditional
UPDATE ... WHERE balance_cents >= $cents RETURNING— an exhausted balance returnsUZ-BILLING-005 CreditExhaustedinstead of producing a partial debit. - Schema slots resequenced contiguously to
001..018to tidy up pre-alpha gaps before the v2.0 baseline.
New: GET /v1/tenants/me/billing — returns the caller's tenant billing snapshot.
{
"plan_tier": "free",
"plan_sku": "free_default",
"balance_cents": 1000,
"updated_at": 1713700000000
}Auth: Bearer Clerk JWT (operator or admin). Returns 401 UZ-AUTH-001 without a valid token.
<Update label="Apr 21, 2026" tags={["What's new", "API", "Security", "Integrations", "Observability"]}>
New users can now sign up through Clerk and have their account provisioned automatically. A Clerk user.created webhook delivered to POST /v1/webhooks/clerk atomically creates a tenant, a user record bound to the Clerk OIDC subject, an owner membership, and a default workspace with a Heroku-style name (jolly-harbor-482) and a 0-cent credit state. Replayed webhooks are idempotent — a re-delivered user.created returns the existing workspace with created: false and makes no new writes.
- Clerk signup webhook at
POST /v1/webhooks/clerk. Svix signature verified inline againstCLERK_WEBHOOK_SECRET; stale timestamps (>5 min drift) rejected. - Heroku-style default workspace names. 1,024,000-combo name space (32 adjectives × 32 nouns × 1000 suffixes) with per-tenant uniqueness guaranteed by a partial index.
- Internal identity model: a new
core.userstable (indexed by Clerk OIDC subject) andcore.membershipstable wire users to tenants with a role. Ready for team accounts in a later release.
POST /v1/webhooks/clerk— request body is a Clerkuser.createdevent envelope; headerssvix-id,svix-timestamp,svix-signaturerequired. Responses: 200{workspace_id, workspace_name, created}; 400UZ-REQ-001(malformed JSON or missing primary email); 401UZ-WH-010(invalid signature); 401UZ-WH-011(stale timestamp); 413UZ-REQ-002(body over 2 MB); 500UZ-INTERNAL-*(operator misconfig or DB error). Non-user.createdevent types are 200-ignored so Clerk stops retrying them.
Three new Prometheus counters on /metrics (six time series): zombie_signup_bootstrapped_total, zombie_signup_replayed_total, and zombie_signup_failed_total with reason label (bad_sig, stale_ts, missing_email, db_error). One new PostHog event, signup_bootstrapped, with distinct_id = oidc_subject so funnels stitch across retries; email domain included, full email never is. Server log lines (clerk.bad_sig, clerk.stale_ts, clerk.bad_request) flow through the existing OTLP log exporter. The operator metrics reference (Metrics reference) has been reconciled against the live exporter.
<Update label="Apr 19, 2026" tags={["Improvements", "UI", "Performance"]}>
Buttons, cards, dialogs, inputs, and other UI primitives now come from a single @usezombie/design-system package. The dashboard and marketing site share one source of truth — tweak a variant once, both surfaces update.
The new /agents page adds an interactive hero and animated terminal. Landing JS is under 90 kB gzipped, with a size-limit CI gate guarding bundle size. PostHog loads on idle so first paint is no longer blocked.
<Update label="Apr 19, 2026" tags={["Improvements", "CLI", "API"]}>
Workspace credentials now flow through a single path: zombiectl credential add writes to the workspace vault, and that's what every zombie reads at runtime. No parallel surfaces, no guessing which command owns a given secret.
<Update label="Apr 19, 2026" tags={["Improvements", "CLI"]}>
The CLI and API now speak one language: zombiectl install → up → status → logs → kill. zombiectl --help is shorter, the API surface is tighter, and the docs, product, and code all describe the same thing. See Zombies.
<Update label="Apr 18, 2026" tags={["Docs"]}>
A new Zombies section walks through installing a template, adding credentials, running, observing, and killing a zombie. Pages describing the legacy v1 pipeline have been retired; the old /specs/* and /runs/* URLs now 404.
New pages: overview, install, running, credentials, webhooks, skills, templates.
<Update label="Apr 18, 2026" tags={["New", "API", "Security"]}>
Tenant admins can now mint named, rotatable API keys via POST /v1/api-keys — scoped to the tenant, revocable, and audited. Raw keys (zmb_t_…) are shown once on creation; only the hash is stored. The legacy API_KEY env var still works as a bootstrap fallback.
Workspace-scoped external agent keys were renamed to agent keys: /v1/workspaces/{ws}/external-agents → /v1/workspaces/{ws}/agent-keys.
<Update label="Apr 18, 2026" tags={["New", "Security", "Integrations"]}>
Every per-zombie webhook flows through one fail-closed middleware that handles URL-embedded secrets, Bearer tokens, HMAC signatures, and Svix multi-signature rotation with constant-time comparisons.
Seven providers ship first-class: agentmail, Grafana, Slack, GitHub, Linear, Jira, and Clerk (via Svix). Onboarding takes one field in TRIGGER.md; secrets are workspace-vaulted and rotate without a zombie redeploy. See Webhooks.
<Update label="Apr 16, 2026" tags={["New", "API", "UI"]}>
Workspace-wide activity feed, operator kill switch for runaway zombies, and per-zombie billing summary that mirrors the workspace view. Billing numbers now come from real execution telemetry (previously zeroed since v0.10).
Ships with six accessible React primitives — StatusCard, EmptyState, Pagination, DataTable, ConfirmDialog, ActivityFeed — and Tailwind v4 semantic design tokens.
<Update label="Apr 16, 2026" tags={["Improvements", "API"]}>
Every list endpoint returns the same { items, total, cursor? } envelope so SDK generators can emit a single Paginated<T> type. Memory reads moved to GET, and openapi.json now documents every route the server exposes — 26 previously undocumented operations are authored in.
<Update label="Apr 16, 2026" tags={["Improvements", "API", "Security"]}>
Identity — workspace, zombie, grant — is now always in the URL path (/v1/workspaces/{ws}/zombies/{id}), and query parameters are reserved for pagination and search. Every handler authorizes workspace membership after authentication; cross-workspace lookups return 404, so the API does not leak the existence of resources you cannot see.
<Update label="Apr 15, 2026" tags={["New", "API"]}>
Redirect a running zombie mid-execution without killing it. POST /v1/workspaces/{ws}/zombies/{id}/steer injects a message into the zombie's event stream — delivered mid-execution if the zombie is running, queued otherwise (300-second TTL).
<Update label="Apr 12, 2026" tags={["New", "Memory", "API"]}>
Zombies remember facts across executions. Memory is row-scoped per zombie and persists in Postgres — a lead-collector zombie doesn't re-research the same lead, a support zombie doesn't re-ask customers their plan. Tools: memory_store, memory_recall, memory_list, memory_forget.
<Update label="Apr 12, 2026" tags={["New", "Integrations", "API"]}>
Zombies — internal or external (LangGraph, CrewAI) — call external services through UseZombie's credentialed proxy. Credentials never leave the platform: injected server-side, stripped from response echoes, and logged to the activity stream.
A zombie requests a grant, humans approve once via Slack/Discord/dashboard, and the grant is reusable until revoked. Launch providers: Slack, Gmail/AgentMail, Discord, Grafana. New CLI: zombiectl agent create|list|delete, zombiectl grant list|revoke.
<Update label="Apr 12, 2026" tags={["New", "Observability", "API"]}>
Every event delivery records token_count, time_to_first_token_ms, wall_seconds, and credit_deducted_cents, queryable per-zombie via GET /v1/workspaces/{ws}/zombies/{id}/telemetry. Each delivery also emits an OpenTelemetry zombie.delivery span that lines up correctly in Grafana Tempo.
<Update label="Apr 12, 2026" tags={["New", "Integrations"]}>
Connect Slack via "Add to Slack" OAuth or zombiectl credential add slack. Bot tokens live in the vault; events and interactions are HMAC-verified with constant-time comparison. Any zombie with a slack_event trigger fires automatically on matching messages.
<Update label="Apr 12, 2026" tags={["New", "Observability"]}>
Every trigger and delivery shows up in Grafana and PostHog. Prometheus exposes zombies_triggered_total, zombies_completed_total, zombies_failed_total, zombie_tokens_total, and a zombie_execution_seconds histogram; PostHog fires zombie_triggered and zombie_completed with tokens, wall-time, and exit status.
<Update label="Apr 12, 2026" tags={["New", "Billing"]}>
Free-plan zombies deduct from consumed_credit_cents after each successful delivery at 1 cent per agent-second; Scale is unlimited and short-circuits without a DB write. Crash replay is idempotent on event_id, and a DB hiccup never drops or double-charges an event.
<Update label="April 11, 2026" tags={["New releases", "Zombies", "API"]}>
Zombies are now two-file directories (SKILL.md + TRIGGER.md) instead of a single .md file.
SKILL.md follows the ClaHub registry format — the same file you upload to the CLI is publishable to the skill registry.
TRIGGER.md carries deployment config: trigger, chain, budget, network policy, credentials.
zombiectl install scaffolds both files; zombiectl up sends them raw to the API.
Skills are now config-driven. The NullCraw executor reads SKILL.md instructions and uses
built-in tools (shell, http, file_read) to call external APIs. Adding a new skill requires
only a new directory — no rebuild of the server binary.
Every outbound request from a Zombie now passes through an AI Firewall before reaching external APIs:
- Domain allowlist — only domains declared in
TRIGGER.mdnetwork.allowcan be reached - Endpoint policy — per-endpoint rules in
TRIGGER.mdfirewall:section (e.g., allow GET, deny POST) - Prompt injection detection — scans outbound bodies for instruction override, role hijacking, and jailbreak patterns
- Content scanning — inspects response bodies for credential leakage and PII (credit cards, SSNs, API keys) All firewall decisions are logged as activity events. Fails closed on errors.
All error responses now use application/problem+json with UZ- prefixed error codes.
Every error code has a stable HTTP status — callers no longer need to parse HTTP status codes independently.
The v1 GitHub PR-solver pipeline has been removed. All /v1/runs/* and /v1/specs endpoints
return HTTP 410 Gone with error code ERR_PIPELINE_V1_REMOVED. Use zombie-native SSE stream
and chat-inject API instead (see v0.5.0 release notes).
Preferred webhook URL format: POST /v1/webhooks/{zombie_id}/{secret}.
Bearer token remains supported as fallback.
All HTTP handler boilerplate (arena setup, request ID, Bearer auth) is now handled by a shared
hx.zig wrapper. Handlers contain only business logic. No user-visible behavior change.
<Update label="April 11, 2026" tags={["New releases", "Zombies"]}>
UseZombie is now a runtime for always-on agents. Two commands, running agent:
zombiectl install lead-collector
zombiectl upYAML frontmatter (trigger, skills, credentials, budget) + markdown body (agent instructions). The CLI compiles YAML → JSON before upload; the server only ever sees JSON. Supports voice-transcribed instructions as the instruction body.
Every zombie gets a stable inbound URL: POST /v1/webhooks/{zombie_id}.
Routing is by primary key — no source name collisions, no JSONB index.
Bearer token auth per zombie. Idempotency via Redis SET NX (24h TTL).
Returns 202 Accepted or 200 Duplicate.
Append-only audit log (core.activity_events). Every zombie action — event received,
skill invoked, response returned — is timestamped and queryable.
zombiectl logs streams the activity log. Cursor-based pagination for replay.
Credentials are resolved from the vault at runtime and injected into the sandbox.
No credentials in config files. Add credentials with zombiectl credential add.
The zombie's conversation context is checkpointed to Postgres after each event. On crash and restart, the zombie resumes from the last checkpoint — no lost context.
zombiectl install, zombiectl up, zombiectl status, zombiectl kill,
zombiectl logs, zombiectl credential add, zombiectl credential list.
core.zombies— zombie registry with JSONB configcore.zombie_sessions— session checkpoint (context upserted after each event)core.activity_events— append-only audit log (UPDATE/DELETE blocked by trigger)
Applied automatically by zombied migrate. No changes to existing tables.
16 v1 endpoints removed from the OpenAPI spec (agents, harness, specs endpoints no longer in v2 path).
POST /v1/webhooks/{zombie_id} added. Mintlify sync required — see API Reference.
make sync-version / make check-version prevent VERSION drift across build.zig.zon and zombiectl/package.json.
- Fixed YAML parser silently dropping array items in CLI config upload
- Fixed UTF-8 truncation splitting multi-byte characters in session context
<Update label="April 6, 2026" tags={["New releases", "Improvements"]}>
Interrupt a running agent without aborting it. Send a message via zombiectl runs interrupt <run_id> <message> or POST /v1/runs/{id}:interrupt — the agent picks it up at the next gate checkpoint. Two modes: queued (next checkpoint) and instant (IPC delivery).
zombiectl run --spec <file> --watch now streams gate results in real time. Reconnect with Last-Event-ID replays only missed events — no duplicate floods. Ctrl+C works cleanly.
zombiectl runs replay <run_id> prints a per-gate narrative for completed runs — exit codes, stdout/stderr, wall time, step by step.
zombiectl workspace billing --workspace-id <id> shows completed, non-billable, and score-gated runs with optional --period and --json flags. Backed by GET /v1/workspaces/{id}/billing/summary.
Every run now produces a full trace tree in Grafana Tempo — query {run.id="<id>"} for a waterfall of agent calls and gate checks. Per-workspace Prometheus metrics: token consumption, run outcomes, and gate repair loop distribution.
Agent runs are now scored on actual memory and CPU usage. Agents that stay within their resource limits score higher. Score formula updated to v2 with real resource data.
SSE id: field on live events changed from sequential counter to created_at Unix milliseconds. Clients parsing Last-Event-ID as a sequence number must update.
<Update label="March 30, 2026" tags={["New releases"]}>
The SSE stream endpoint is live: GET /v1/runs/{id}:stream emits gate results in real time as the agent works. CLI support (--watch) is coming in a future release.
Replay any finished run step by step via the API: GET /v1/runs/{id}:replay returns a structured gate narrative with exit codes, stdout/stderr, and wall time. CLI support (zombiectl runs replay) is coming in a future release.
<Update label="March 28, 2026" tags={["New releases"]}>
Set token budgets, wall-time limits, and repair loop caps on each run. Runs that exceed limits are cancelled automatically.
<Update label="March 25, 2026" tags={["New releases"]}>
A complete OpenAPI 3.1 specification covering all 43 API endpoints is now published.
The CLI is now available as a scoped npm package.