Mock-up · spec for building

Cron jobs and the API log

Two features from the top of improv.md, drawn so they can be built from. The log screens use real entries from /api/operations on the running Space — real names, real counts, real timings — because that log already exists and this is mostly a view over it. The cron screens are invented. Nothing here is wired up; every panel is a picture.

Settings → Cron

Setting one up

A cron sends a prompt to an agent on a schedule. Six fields. The job has its own name, separate from the agent's — one agent can carry several jobs, and the list has to be readable when it does. Run on restart exists because the Space sleeps: a job that only trusts the clock quietly does not happen.

mock · settings, cron tab
Settings
GeneralUsageSkillsCronAPI log
Job name
nightly deploy check
What this job is for. Separate from the agent — one agent can have several jobs, and this is the name the list, the log and any failure message use.
Agent name
nightly-index
No agent by that name yet — it will be created when the job first fires, in workspaces/nightly-index. An existing name means reuse that agent.
Agent type
Claude CodeCodexGemini CLIopencodeHermesOpenClaw
The same list and the same names the new-agent dialogue offers — from /api/clis, minus the panes that are not agents (shell, files, trace, remote). A type that is not installed on this Space appears greyed there and here.
Prompt
Check last night's deploy log. If the nightly job took over 60s, find out why and open a PR.
Sent as a normal prompt. The agent keeps its history between runs, so it will remember yesterday unless the prompt says otherwise.
Interval
HourlyEvery dayWeekdaysWeeklyCustom cron…
at 09:00 · Europe/Zurich → 0 9 * * *
Presets write the cron line for you; Custom lets you type one. The Space's clock is UTC, so the zone is stored with the job.
Run on restart
YesNo
The Space sleeps and restarts, and a timer cannot fire while the app is down. With this on, the job runs once when the app comes back. Nothing is replayed — a fire missed while the Space was asleep stays missed.
Create jobCancel

Seeing what is scheduled

The list is what gets used daily: what is due, how the last run went, and three actions per row. Run now fires it outside the schedule — the "after a restart" case. Stop switches the job off but keeps it, so it can be turned back on. Delete removes it. Stopping and deleting are different acts and both are one click away.

mock · settings, cron tab — existing jobs
JobAgentTypeIntervalStateNextLast runActions
nightly deploy checknightly-indexClaude Code every day 09:00 · on restart running tomorrow 09:00 ok 4m 18s · today 09:00 Run nowStopDelete
weekly dependency bumpdep-botCodex Mondays 07:00 running Mon 07:00 ok 11m 02s · Mon 07:00 Run nowStopDelete
digest of yesterdaynightly-indexClaude Code every day 18:00 stopped ok 2m 40s · Tue 18:00 Run nowStartDelete
inbox triagetriageGemini CLI hourly running in 24m failed no such agent type · Tue 14:00 Run nowStopDelete
Two jobs on one agent is the first row and the third — same agent nightly-index, different jobs, different schedules. That is why the job needs its own name. State is running or stopped; the last run reads ok or failed with the reason.
The other entry point

The same thing over the API

Agents set these up the way they use everything else, and the call is logged like any other write — so a cron created by an agent appears in the API log below, attributed to that agent.

# create — the agent is made on the first fire if it does not exist
POST /api/crons?from=$AM_ID
{
  "name":         "nightly deploy check",     // the JOB's name
  "agent":        { "name": "nightly-index", "cli": "claude" },
  "prompt":       "Check last night's deploy log…",
  "schedule":     { "cron": "0 9 * * *", "tz": "Europe/Zurich" },
  "runOnRestart": true
}
→ 201 { "id": "cron_7f3a", "next": "2026-08-20T07:00:00.000Z" }

# list, with how each one last went
GET  /api/crons
→ 200 { "crons": [ { "id": "cron_7f3a", "name": "nightly deploy check", "agent": "nightly-index",
         "state": "running", "next": "…",
         "last": { "at": "…", "status": "ok", "durationMs": 258000 } } ] }

# fire one by hand — the "after a restart" case
POST /api/crons/cron_7f3a/run?from=$AM_ID     → 202 { "ok": true, "agentCreated": false }

# stop keeps the job; delete removes it
PUT    /api/crons/cron_7f3a?from=$AM_ID  { "state": "stopped" }
DELETE /api/crons/cron_7f3a?from=$AM_ID
Settings → API log

The list view

This half is nearly free: /api/operations already records every write with who made it, what it hit, the status and how long it took. One line per call, so a screen holds twenty of them. Rows below are real.

mock · settings, api log — real entries
API log 193 calls · 15–19 Aug
ListMap
Everyonelvwerramanager Only failures (3)PromptsFiles path contains…
TimeWhoCallStatusTookPayload
20:45:07lvwerraPOST /api/agents/agent-manager-2-93de86/prompt20018.4sprompt · 1,204 chars
20:42:11lvwerraPOST /api/sessions/manager-1049f7/input200576msprompt · 196 chars
20:41:02managerPOST /api/agents/am-overview-improv-0c2edd/prompt20020.2sprompt · 2,551 chars
20:38:55managerPOST /api/agents/agent-manager-4-ba3fbf/prompt20012.0sprompt · 3,090 chars
16:25:50lvwerraDELETE /api/sessions/sfo-departures-93007d/attachments/att_3b61b6ef2006ms
16:24:18lvwerraPOST /api/files/files-5-a39b19/write20041msfile · 8.2 KB
11:07:33managerPOST /api/sessions/nonexistent-xyz/archive4043ms
10:04:18lvwerraPOST /api/sessions/claude-code-3-30bb27/input200302msprompt · 10 chars
The prompt text is not in the log — only that there was one, how long it was, and a checksum. So this view answers who asked whom to do something, and when, never what they said. Matching checksums mean identical prompts, which is what a cron produces and a cheap way to spot one.

The visual view: connected swimlanes

One lane per agent, time left to right, and the interactions drawn between the lanes. A prompt is an arrow from caller down to target. A finished wait is an arrow back the other way. So the picture shows work going out and attention coming back — which is the thing a flat list cannot show.

lvwerra manager agent-manager-2 am-overview-improv files-5 prompt prompt prompt finished reported file write prompt finished 19 Aug 20:38 → 20:46
—▶ prompt (caller → target) ⇠ ⇠ wait resolved (target → caller) the call's own lane hovering a line would show time, status and payload from the same row as the list

Also considered: a hub-and-spoke flow map (shows the fleet's shape, loses time) and a caller × target grid (cheapest, loses both time and any sense of a chain). Lanes win because they carry time and direction at once, which is what "who called who, when" actually asks for.

The return arrows cannot be drawn today — here is the smallest fix

must be in scope

Why: only writes are logged. operations.js:8 defines MUTATING = POST, PUT, PATCH, DELETE and line 78 skips everything else, so of the last 193 calls 173 were POST, 20 PUT and zero GET. Waiting is a GET (/api/agents/:id/wait), so every arrow coming back is missing from the data. Build the lanes on today's log and you get a picture of work being handed out and nothing ever returning.

The smallest change — one allowlist, one guard, no new storage:

// operations.js — log the reads that mean "A waited for B", nothing else
const LOGGED_READS = [/^\/api\/agents\/[^/]+\/wait$/];
const shouldLog = (req) => MUTATING.has(req.method)
  || (req.method === 'GET' && LOGGED_READS.some((re) => re.test(req.path)));

// …and inside record(), drop a wait that carried no news:
// wait is a polling loop — only the call that RESOLVED is an event
if (req.method === 'GET' && responseBody && responseBody.matched === false) return;

Three details that decide whether it works:

1. Do not log tail. It is called constantly by every open pane; logging it multiplies the log by the polling rate and adds nothing the resolved wait does not already say.

2. wait carries no ?from= today — it is documented as read-only, and the middleware rejects mutating calls without one (400). Logged reads must stay optional on from, or every running watch loop breaks the moment this ships. Record the origin when it is there; when it is not, the entry still says someone finished waiting on B, which draws as a mark on B's lane rather than an arrow.

3. Then ask agents to pass it. Adding ?from=$AM_ID to the wait examples in the shared environment skill is what turns those marks into real arrows. It costs one line of documentation and no code.

Volume: a resolved wait is one entry per finished watch — roughly one per prompt, so this grows the log by about a third, not by orders of magnitude. That is the whole cost of making the headline view honest.

Feasibility and loopholes

Where each half is easy because something already exists, and where it is genuinely hard.

The log list is mostly done already

easy

/api/operations exists and records exactly the fields these mocks show. The list is a settings tab over data already on disk — no new server work, no new storage. Every row above came from the real endpoint, which is why the names look like your fleet.

Firing a prompt on a schedule is a small piece of code

easy

Prompting an agent and creating one are both existing API calls. A cron is a stored line, a timer, and a call the app already makes — plus the honest bits: recording what happened, and being visible when it fails.

Restarts: a toggle, not a replay

decided

A timer cannot fire while the app is down, and the Space sleeps. The answer is the Run on restart toggle plus Run now in the list — no catch-up machinery, and a fire missed while the Space was asleep stays missed.

Known behaviour, deliberately without controls

decided

Two things a later reader should know, both accepted as-is: a prompt sent to an agent that is still working lands in its composer and may be picked up mid-task, and a schedule spends tokens for as long as it exists — an hourly job is 8,760 runs a year whether anyone reads the output or not. Neither gets a guard or a ceiling. The last-run column and the Stop button are the levers.

The log stays readable through filters, not trimming

decided

No rotation, no retention screen. What keeps 193 entries — or 19,300 — usable is filters that match the questions people ask: only failures, only this agent, only prompts. Worth knowing how lopsided it is: 3 of 193 calls failed, so Only failures earns being one tap rather than a search.

Two small traps in creating the agent

medium

Name collisions: if the job names an agent that already exists, this design reuses it — which is what someone setting up a daily job means, and it is written on the form rather than left to be discovered. Missing agent type: the failed row above says no such agent type. Types are installed in the image, so a job can outlive the CLI it names; the failure has to be legible in the list, because that is the one place anyone will look.