Skip to content

Changelog

Versions follow the wheel on PyPI. Each entry lists what changed and, where a downgrade needs care, how to do it.

Changelog

3.3.1 (2026-10-07)

  • The overlap soak fits the engines the server allows. Since 3.0 the server caps max_engines at the CPU count, so on a four-CPU runner the soak's fifteen flows queued for engines and failed every check. benches/soak_overlap.py now keeps flows in priority order (one per policy and a control first) while their max_concurrent caps fit the engine budget, and prints the ones it leaves out.

3.3.0 (2026-10-06)

  • The UI has a sidebar. The sections move from the top bar to a sidebar on every page, grouped as Operate (Dashboard, Runs, Queue), Build (Flows, Variables, Rules), Observe (Events, Artifacts, Workers) and System (Settings). The project and group tree becomes a scope picker at the top of the sidebar that you can type into. The top bar keeps search, the live indicator and the theme toggle, and adds New run. Every URL and ⌘K work as before. Tour of the UI
  • A quieter dashboard that opens logs in place. Counts that are zero (Running, Crashed, Late) are hidden, and the proportion bar is gone. A new Flows card shows each flow's last ten runs as squares; click one, or a run in Recently completed, to read its logs on the dashboard. Upcoming, Needs attention and Running now share one row, and the queue-depth sparkline moves to the Queue page.
  • Run, Flows, Queue and worker pages lead with what matters. The run page says in one sentence how the run went, lists its facts in labelled cells, shows a failed run's error with a link to its first error line, offers Cancel only while the run is live, and counts log lines per level. The Flows page has Failing, Scheduled, Waiting for input and Never run chips, and a flow's upstreams, tags and health move under its name. The Queue page shows capacity first and one Up next list. The worker status page opens with a health sentence and lists failing flows first.
  • Fix: flows in sub-folders failed on the server's engines. A flow in flows/financing/app.py under the served directory passed cereyan check and ran on workers, but every run on the server ended Failed with ModuleNotFoundError: No module named 'flows': the flow recorded its own folder as its source directory instead of the directory its dotted module imports from. It now records the import root, so the server's engines and workers import it alike, editing it recycles its engine, and the MCP get_flow_source tool finds it. Stored flows pick up the new directory the next time they register. The same goes for the other ways in: cereyan run flows/financing/app.py:etl imports the file by its path from the current directory, so its own imports by that path work; a flow in a module started with python -m flows.financing.oneoff records that name, so its runs no longer depend on the server's working directory; and get_flow_source reads a flow defined in a package's __init__.py. Flows in sub-folders
  • cereyan check reports a source error for a flow whose module, imported from its source directory alone, does not load its own file.

3.2.0 (2026-10-05)

  • Artifacts are paged. The Artifacts page, a key's history, and a run's or task run's Artifacts tab show a page at a time, newest first, with Previous and Next, in place of a list that grew with every Load more. GET /api/artifacts takes task_run_id beside run_id, so a task run's artifacts page like the rest. Artifacts
  • Runs say where they ran. The Runs list has a Host column, the server or the worker's name with the processor slot, and the run page says the same in its header. Both link to the Workers tab. Run across machines
  • Client.run_state(run_id, scope=None, key=None) reads what a run stored with publish_state and task_state, wrapping GET /api/runs/{id}/state.
  • Documentation. A guide and an example for receive(topic), messages and publish_state, which 3.1 added without a page: Send a message to a running flow. The tour covers the Queue page and the Workers tab. The events reference lists worker.registered and worker.offline, which its generator had dropped, and the worker.updated stream message. The Python API page lists receive and publish_state.

3.1.4 (2026-10-03)

  • Fix: a continuous schedule stopped looping after a server restart. A run whose engine died while the server was down was marked Crashed without a retry, and a Crashed run with no retry still counts as the loop's active run, so no next run was ever created. Startup now sends such runs through the crash-retry chain, and schedules again any retry that was waiting when the previous server stopped.
  • Fix: a database write could be reported as saved after it was rolled back. Writes are committed in groups. If the commit failed, every caller in the group still got a success reply. They now get an error. A write that fails partway is also rolled back on its own, instead of committing its partial changes with the rest of the group.
  • Fix: a run could be lost between the queue and its engine. If handing a run to an engine failed, or the engine's request was dropped, the run left the queue but never reached the engine, and its resources stayed held. It now goes back in line with its resources released.
  • Fix: "Run now" on a continuous schedule queues the run at once, and the schedule no longer shows a next run time in the future or one that has already passed.
  • Long-running servers: cancelled timers no longer leak memory. Continuous schedules no longer wait on each other's locks. Engine polls no longer check the module file while holding the queue lock. The supervisor's half-second check no longer copies every queued run. Schedule lists use one database query instead of one per schedule.

3.1.3 (2026-10-02)

  • Fix: an idle processor could sleep for up to 30 seconds with a run waiting for it. An engine registered for its wake-up only after finding no work, so a run that became ready in between, because another run finished or a resource was released, waited until the engine's poll timed out. The same fix applies to a task waiting to acquire a resource.

3.1.2 (2026-10-02)

  • 3.1.0 and 3.1.1 were tagged but never published. Their CI runs failed: first on lint and a test that only builds on Unix, then on a newer clippy lint, a worker that stalled on a reverse DNS lookup on macOS, and the race below. 3.1.2 is the 3.1 release with those fixed. The notes below describe 3.1.
  • Fix: Join the line now could leave a continuous schedule's run waiting the full delay. Joining just as the loop created its waiting run moved the run to now, and the loop then put it back at its original time. The schedule could also report a waiting loop with no next fire for a moment.

3.1.1 (2026-10-02, not published)

  • 3.1.0 was tagged but never published. Its CI run failed on a lint step and on a test that only builds on Unix, so the release was held back.

3.1.0 (2026-10-02, not published)

Messages to running flows, a status page on every worker, and worker fixes.

  • receive(topic) and messages. A flow can wait for a message on any topic: receive("approval") pauses the run, frees its engine, and resumes it with the payload of POST /api/runs/{id}/messages/{topic}. A message sent before the run reaches receive is queued and handed over when it does. receive(topic, timeout=60, default=...) resumes with default when nothing arrives in time; the timer is the server's, so the run holds no engine while it waits. wait_for_input(prompt) is now receive("input", prompt=prompt). The Python client gains send_message, and the MCP server list_waiting_runs and send_message.
  • publish_state(key, value). A flow stores a JSON value under a key for its run, readable with GET /api/runs/{id}/state while the run goes on.
  • A status page on every worker. cereyan worker serves a read-only page, built from the same UI bundle as the server's: its state, runs completed and failed since it started with a bar per flow, each processor's engine and run, its last 20 messages, and its host details. When the server cannot be reached, the page says so, keeps the last counts marked "as of" when they arrived, and goes on showing what the worker knows itself. status.json returns the same data, and healthz answers 503 after three missed heartbeats. The page binds to 127.0.0.1 on a port the OS picks, which the worker logs at start and the Workers tab shows. Choose another address with --status-host and --status-port (or [worker] status_host and status_port). Links are relative, so a reverse proxy can serve the page under any path. A port that cannot be bound stops the worker before it registers. The worker now opens this one port. It used to open none. Run across machines
  • Heartbeat answers carry stats. POST /api/workers/{id}/heartbeat also returns the worker's runs per flow and state since its reported start time, and the run each of its engines is executing. Workers from 3.0 ignore it.
  • A worker that stops cleanly leaves at once. On SIGTERM or Ctrl-C a worker drains, waits for its runs, and then calls the new POST /api/workers/{id}/leave, so it shows Offline at once and comes back online when started again. A drain set before the stop is kept across the restart.
  • A worker cannot be named server. That name is the server's own processors in the queue and the UI; registration refuses it and asks for another --name.

  • Fix: a worker whose heartbeat failed while its checkout changed sends the new fingerprints on its next heartbeat. Before, the server kept the old ones and could hand that worker runs of code it no longer had.

  • Fix: chunked result uploads. Two workers uploading the same key, or a retried chunk, could corrupt the stored result. Each upload now writes its own file, and each chunk lands at its offset (upload and offset query parameters on PUT /api/results/{key}).

Migration. The store migrates to schema 23 (run_message, and indexes). Workers and servers of 3.0 and 3.1 mix; a 3.0 worker uploads results without upload and offset, as before.

3.0.1 (2026-09-29)

  • 3.0.0 was tagged but never published. Its Windows test run failed, so the release was held back. 3.0.1 is the same release plus test-suite fixes: test workers start in their own process group so a worker can drain on Ctrl-Break, tests that need four processors skip on smaller runners, and rule-firing tests wait for the firings to finish. The notes below describe 3.0.

3.0.0 (2026-09-29, not published)

Remote workers, a Queue page with processors you set while the server runs, and continuous schedules.

  • Remote workers. cereyan worker --host URL on another machine adds its processors to the server's queue. The worker runs its own checkout, registers the flows it can run with a fingerprint of each module, and connects out: it opens no port. When the server's processors are full, a run goes to a worker whose code for the run's module matches the server's, unless the flow says @flow(runs_on="server"). Results, cache entries, checkpoints and secrets stay on the server; the worker's engines use it for all of them. Every run records the host and processor it ran on. The Queue page gains a Workers tab (status, host metadata, a six-hour schedule per processor, drain, resume, forget) and groups processor tiles by host; the MCP list_workers tool and server_health report workers. Run across machines
  • Leases. Every hand-off of a run to an engine carries a lease; a report, heartbeat or transition under an older lease is refused with 409 and the engine stops the run, so a run rerun after a network split is not also finished by its first engine.
  • Code fingerprints. Every run records source_hash, the fingerprint of the module that ran it (roadmap M5.7), and cereyan check reports each flow's fingerprint and runs_on.
  • Engines use their server for everything. An engine started by a server or a worker reads variables, emits events outside a run and hands off subflows through its server, never through a store of its own; a worker's engine asks the server to decrypt secrets.
  • Events worker.registered, worker.offline, and run.local_path_on_worker; worker. is a reserved event prefix.
  • TLS trust from the environment. The engine client verifies certificates against SSL_CERT_FILE and SSL_CERT_DIR when set, as curl and Python do, so a worker reaches a server behind a proxy with an internal CA.
  • A Queue page, and processors you add and remove while the server runs. A new top-bar section between Runs and Flows shows the one queue every run waits in: each run in dispatch order with why it can or cannot start and how many runs went ahead of it, the runs joining the line in the next hour, and the engine pool as processors, each running, idle, or draining, with its run and module. Add or remove processors there or with PATCH /api/settings {"max_engines": n}; the change applies at once, is written to [server] max_engines, cannot exceed the CPU count, and never interrupts a run: a busy processor that is removed finishes its run first. GET /api/queue returns the same view, and the MCP server_health tool reports the processor count. Engines and the home directory › Processors and the queue
  • Continuous schedules. Continuous(delay="30m") is a fourth schedule kind next to Cron, Interval and RRule: when a run ends, the next run joins the line delay later, so at most one run of the schedule is ever waiting, in line, or running and the wait holds no processor. It is seeded when the schedule starts or resumes, keeps its waiting run across a restart, and is never marked Late. Pause the schedule to stop the loop; POST /api/schedules/{id}/now (Join the line now on the flow page) moves the waiting run to now. The schedule editor has a Continuous tab, the flow page shows where the loop is, the Queue page pauses and resumes loops, and the schedule API, list_schedules and the MCP schedule tools report the loop state. Schedules › Continuous schedules
  • disable_after can count failures in a row. A window of None, as in disable_after=(3, None, 6 * 3600), disables the flow after that many failures in a row, and a Completed run resets the count. Windowed declarations behave as before.
  • The queue keeps its order across modules. A free engine takes the first run in line that can start, whatever its module. A deep backfill of one module no longer keeps a waiting run of another module from starting.

  • Fix: a served run can read a variable whose name contains /. Variable.get("warehouse/api_token") inside a served run, or from a script while a server runs, put the name into the URL unencoded, reached a route that does not exist, and returned the default, so the run saw None without an error. Variable.unset had the same fault. The name is now sent as one encoded path segment. Offline reads were not affected.

Breaking. The engine protocol carries a lease (WorkItem.lease, X-Cereyan-Lease); engines and servers of 2.x and 3.0 do not mix. The store migrates to schema 17 (worker, worker_flow, and run.host, run.processor, run.lease, run.source_hash). EngineKey matching across machines ignores the source directory. worker. names are reserved for events.

Upgrade note: one processor by default. A server now starts with one processor unless --max-engines, app.serve(max_engines=), or [server] max_engines says otherwise; it used to start with one per CPU. Runs that used to execute side by side now wait their turn until you add processors on the Queue page or set [server] max_engines.

2.3.1 (2026-09-28)

  • A changed code schedule no longer keeps the old one's runs. When a flow's code changed its schedule and the server restarted, the spec was updated in place but the runs already materialized under the old spec stayed Scheduled, and look-ahead continues from the latest of them. A flow moved from daily at 00:10 to hourly at :20 kept its three nightly runs and got no hourly run for three days. Those runs are now dropped at start, as an edit through the API already dropped them. Catch-up, jitter and deadline changes keep the runs, because they do not move fire times.

2.3.0 (2026-09-20)

Documentation only. The library, the CLI, the server and the UI are unchanged from 2.2.0.

  • A landing page on the documentation home. The home page is now a landing page rather than a list of links: the offer in one sentence over the pipeline example, the three ways to run the same file on a band carrying the brand mark, the dashboard in a browser frame in both schemes, the four pillars, sections on targets, rules and backfills, the performance targets the benchmarks enforce, and one call to action repeated down the page. Its only prose is the pipeline example itself, so the code on the front page is the code the documentation tests run. Home
  • One type scale across the site. Landing typography sits on the same ladder as the documentation pages, a major third anchored at the theme's body copy, declared once as tokens. Material sets a 125 percent root, so the landing page had been rendering a quarter larger than it was drawn; body copy, tables, buttons and small labels all come back onto the scale, and the documentation pages are untouched.
  • Documentation chrome in step with the app. A hairline under each section heading, markers on the table of contents, a filled pill on the current page in the section nav, and admonitions as a plain bordered box with a label row instead of a coloured frame and a tinted title. Each type keeps its own icon colour, so a warning still reads differently from a note.

2.2.0 (2026-09-20)

  • Freshness and deadlines. @flow(fresh_within=, expect_by=, expected_duration=, overdue_factor=) derive a PASS, WARN or FAIL health per flow from its recorded runs, shown in a Health column on the Flows page and as health on every flow summary; a Running run past its expected duration records run.overdue once, for rules to page on. Detect when something did not happen › Freshness and deadlines
  • Backfills take explicit values, skip what is done, or restate. values lists the parameter values instead of a range, missing_only leaves out values whose latest run completed, and force tags the runs cereyan:force so they ignore targets, caches, checkpoints and bulk_complete. On the API, Client.backfill, cereyan backfill --values/--missing-only/--force, the MCP tool, and the dialog. Backfill a date range › Pick the values
  • Debounce and throttle. Unique(on_conflict="debounce", debounce=30, max_wait=300) creates a run for later and moves it along with the latest parameters on every further submission of its key until it starts; Unique(on_conflict="throttle", period=60) keeps the first run of a key per sliding window and answers the rest with it. Both apply wherever runs are created. Limit concurrency and overlap › Keep runs unique
  • Durable waits. sleep, sleep_until, wait_for_event(name, match, within), wait_for_target(target, poke, timeout), and raise Snooze(seconds) end the attempt as Sleeping, AwaitingEvent, or AwaitingTarget, free the engine and the run's resources, and resume when the time, the event, or the target comes, replaying the completed prefix from checkpoints; wakes survive a restart, WaitTimeout bounds a wait, and the run page offers Wake now. Offline they block. Wait without holding an engine
  • Keyed resources. A resource name can be a template over the run's parameters (resources={"api:{{ tenant }}": 1}), a [resources] key with * gives every matching name its own total ("api:*" = 1), and "tag:gpu" = 1 limits every run tagged gpu whatever its flow. Keyed instances show in the resources snapshot while in use, and cereyan check accepts a templated name covered by a pattern. Limit concurrency and overlap › Keyed resources
  • Task state store. cereyan.task_state.get/set/delete/items keep small JSON notes (up to 64 KB) scoped to the run and the task, such as an external job id, that survive the task's retries, later passes, crash reruns, and retries from failure, and are deleted with the run. Listed on the run page's Details tab, GET /api/runs/{id}/state, and the MCP get_run tool. Tasks › Task state
  • Unique runs and idempotent creation. @flow(unique=Unique(key="{day}", period=, states=, on_conflict="skip"|"replace")) keeps at most one run per key at a time, wherever the run is created: the API answers 200 {"conflict": true, "run"} with the run that holds the key instead of creating one, Client.run returns it with conflict: True, the MCP run_flow tool and a rule's run_flow report it, and an offline call returns None. idempotency_key and idempotency_ttl on run creation do the same for a repeated request. Dependency-triggered runs are keyed by their upstream run. Limit concurrency and overlap › Keep runs unique
  • Retry a finished run from where it failed. POST /api/runs/{id}/retry with from (failure, start, or a task's key) creates a new run linked to the original that replays its completed tasks from their checkpoints and executes from that point and everything downstream. Also Client.retry(), cereyan retry <run> [--from KEY], the bulk action retry, the MCP rerun_run tool's from, "Retry from failure" on the run page, "Rerun from here" on each task, and Retry on the Runs page. Retry, time out and survive crashes › Retry a finished run
  • Runs for later. POST /api/runs and POST /api/flows/{id}/runs take delay (seconds) or scheduled_time; the run waits as Scheduled with that time, appears under the flow's upcoming runs, and starts when due with the same pre-warm, Late marking, and global-pause hold as a schedule fire. Client.run(at=, delay=), cereyan run --at/--in (hands the run to the running server), the rules run_flow action's delay, the MCP run_flow tool's at and delay_seconds, and a "Start at" field in the flow page's run dialog. Schedule a flow › Run once, later
  • Task checkpoints: a crash rerun or a resumed run picks up after the last completed task. Every completed task's result is stored as a checkpoint of its run (task_run.result_ref, input_hash), and a crash rerun or a resume after wait_for_input replays matching tasks as Replayed task runs (task_run.replayed) instead of executing them, stopping at the first task whose arguments changed. @flow(checkpoint=True) also replays in-process retries, checkpoint=False turns it off, and checkpoint_max_bytes (50 MB) bounds what is stored; [defaults] retain_checkpoints_days (7) removes old checkpoint files. Pausing for approval no longer needs cache=INPUTS on the prefix. Runs and states › Checkpoints
  • Compare two runs. GET /api/runs/compare?ids=a,b returns what changed from the first run to the second: parameters and attributes, the duration delta, each task's state and duration with the first task that diverged, the error lines and failure message that are new, and artifact differences. Client.compare_runs(), cereyan runs compare A B, and the MCP compare_runs tool return the same document; the Runs page offers Compare for two selected runs and a run's menu compares it with the previous run of its flow. Runs and states › Comparing runs
  • Cancel runs by selector, and pause every schedule at once. The cancel_runs rule action cancels the active runs a templated selector matches (flow, parameters, states), never the event's own run, so orders.cancelled can stop that order's work. POST /api/scheduler/pause (reason, until, suppress_rules) holds every schedule while running, manual and backfill runs continue; held runs start on resume and each schedule catches up under its own policy; the pause survives a restart. With suppress_rules, rules record their firings as suppressed instead of acting. The UI shows a banner with a Resume button and a Scheduler card in Settings; cereyan pause and cereyan resume, Client.pause_scheduler(), and the MCP tools pause_scheduler and resume_scheduler do the same. Events scheduler.paused and scheduler.resumed. Schedules › Pausing everything
  • Alerting: consecutive failures, recovery, backfill completion, webhook presets, signing, and run links. run.failed and run.crashed carry failures_in_a_row; flow.recovered fires when a flow succeeds after failing; backfill.completed fires when a backfill's last run ends; rules take after_consecutive. A webhook action's preset (slack, teams, discord, ntfy, telegram, pagerduty) supplies the message, secret adds Standard Webhooks signature headers, and {{ run.url }} links to the run under the new [server] public_url. Firings are recorded before their actions run and updated as each completes. React to events with rules
  • Search runs by parameter or attribute, and act on everything a filter matches. GET /api/runs?params=day=2026-09-01 and ?attributes=region=eu filter by exact value (the last 30 days unless a start bound is given); set_attributes(**values) in a flow or task records searchable attributes on its run; POST /api/runs/bulk cancels, reruns, or deletes every matching run after a dry-run count. The Runs page has the parameter filter and an all-matching action row; the MCP list_runs tool takes params and attributes. Runs and states › Finding runs
  • Secrets read with Variable.get are masked in run logs and failures. Log lines, captured prints, exception text, and a failed run's message and traceback show *** in place of any secret value the process read, for values of four characters or more, offline and served. Artifacts and results are not masked. Store variables and secrets
  • Two more overlap policies and three schedule knobs. on_overlap="cancel_old" cancels a flow's older runs so the newest takes the slot, and "buffer_one" keeps at most one run waiting. Schedules take catchup_window (missed fires older than this are not caught up, reported as expired), jitter (a deterministic per-run offset), and start_deadline (a run not started in time is skipped with reason missed_start_deadline); start_deadline also works on the flow. All three are on Cron, Interval, and RRule, the schedule API, the MCP schedule tools, and the editor. Limit concurrency and overlap · Schedules
  • GET /api/metrics in the Prometheus text format: runs by state and flow, task runs, queue depth, engines, resources, rule firings, schedules, store size and commits, and histograms of schedule-to-start delay, resource wait, and commit latency. [server] metrics_public (--metrics-public, CEREYAN_METRICS_PUBLIC, app.serve(metrics_public=True)) serves it without a token. The dashboard shows the last hour of queue depth from GET /api/metrics/history. Monitor with Prometheus
  • Retention can delete runs, and backups are scheduled, on demand, and automatic before a migration. [defaults] retain_runs_days, retain_failed_runs_days, and keep_last_runs_per_flow let the hourly pass delete old terminal runs with everything they recorded, sparing running runs, live backfills, and each flow's newest runs; backup_every and backup_keep write and prune db-*.sqlite copies; cereyan backup and POST /api/database/backup write one now; and a store older than the library is copied to backups/pre-migration-v<n>-<time>.sqlite before it is migrated. All settings are editable on the Settings page. Clean up the store
  • Ten more MCP tools, every tool annotated, and a read-only mode. list_backfills, get_backfill, cancel_backfill, get_flow_source, server_health, list_variables (secret values never returned), list_resources, flow_dependencies, check_flows (runs cereyan check on the served directory), and rerun_run. Every tool carries readOnlyHint and destructiveHint. [server] mcp_read_only (--mcp-read-only, CEREYAN_MCP_READ_ONLY, app.serve(mcp_read_only=True)) lists and allows only tools that change nothing. @flow(mcp_tool=True) publishes a flow as a flow__<project>__<name> tool with its parameters as arguments. Prompts health_check and plan_backfill join diagnose_run. Use cereyan with an AI agent
  • cereyan check [dir] imports a directory as serve would and reports what would stop it serving cleanly, without opening the store: import failures, unknown after= upstreams, schedules the core rejects (with the next three fires of every valid one), resources missing from [resources], and custom routes that collide with the built-in API. --json prints one object for CI, --strict makes warnings fail, and --now fixes the preview's reference time. The same report is cereyan.check.check_directory(path). Test a pipeline › Check the directory before serving
  • A new mark. The brand mark is one wave of alternating current, the sign for the word cereyan, in the app's top bar, the app's favicon, and the docs' logo and favicon. The docs now use the app's Geist type and warm neutrals in both schemes, and show the dark screenshots in the dark scheme.

2.1.0 (2026-09-19)

Upgrading from 2.0

  • A server bound beyond loopback without a token now requires one it generates. With host = "0.0.0.0" or any non-loopback address and no token, the server creates <home>/token on its first start, reuses it after, and requires it on /api/* and /mcp. Scripts, the CLI, and cereyan mcp on the same machine read the file through server.json and need nothing; a browser or script elsewhere sends the file's contents as before with a configured token. Set allow_unauthenticated to keep serving without a token, which the server warns about as 2.0 did. Secure the server

Changes

  • server.json and GET /api/server report exposed (bound beyond loopback with no token) and token_file (the generated token's path); the UI shows a banner on every page while the server is exposed. server.json

2.0.0 (2026-09-16)

Upgrading from 1.x

  • Back up the store first. The first start rebuilds the task_run table and takes longer, and 1.x cannot open the result; copy db.sqlite with the server stopped. Install › Upgrade
  • Task runs are numbered per pass. A retried or resumed execution restarts at step-0 rather than continuing at step-1; read pass or pass ?pass= to /api/runs/{id}/tasks, /graph, and /api/task-runs where you relied on the old names. Runs and states › Attempts
  • A server opened under a host name needs it in allowed_hosts. Requests whose Host or Origin names an unknown host now get 403; add LAN names, tunnels, and reverse-proxy hosts under [server] allowed_hosts. Secure the server
  • POST /mcp requires Content-Type: application/json. Other types get 415; MCP clients and cereyan mcp already send it.
  • An rrule schedule with a zone-less DTSTART fires in its timezone=. It used to fire in the machine's zone; check its fire times, and expect an rrule schedule that had stopped after its first fire to resume. Schedules
  • Projects and groups are chosen in a sidebar. The top bar's project switcher, and the Flows page's Project and Group filters and Dependencies panel, are gone; pick a project or group in the scope sidebar, and read upstreams in the new Starts after column. Tour of the UI
  • Answers stored by 1.x are not read. A run answered under 1.x whose next attempt had not started before the upgrade asks its question again; answer it once more.
  • [defaults] max_engines and engine_max_runs are gone. They now draw an unknown-key warning and are ignored; move them under [server]. Configuration
  • A group name used in two projects is two groups, one under each project, instead of one section spanning both. App and projects › Groups

Changes

  • A scope sidebar lists every project with its groups beside Dashboard, Runs, Flows, Events, and Artifacts, and the Flows page is redesigned around it. Tour of the UI
  • Flows, runs, task runs, and artifacts filter by group in the API, cereyan runs ls --group, and the MCP list_flows and list_runs tools. Organise flows into projects and groups
  • retry_on and retry_when choose which failures a flow or task retries, and raise Abort(...) ends it at once. Retries, timeouts, and crashes
  • A flow that calls wait_for_input twice waits for the second answer instead of reusing the first, and GET /api/runs/{id}/input lists the answers so far. Pause a run for approval
  • A resumed or rerun run records the task runs of every execution instead of losing those after the first.
  • One fire of a schedule creates one run, even after the machine slept through its look-ahead.
  • A daily or longer interval no longer skips the day the clocks go forward; it fires at the first instant after the gap. Schedules › Daylight saving time
  • An rrule schedule keeps firing after its first run.
  • One rejected event no longer discards a whole engine report; the server records run.report_rejected.
  • A web page in your browser can no longer drive a local server through cross-site requests or DNS rebinding. Secure the server
  • New guide Organise flows into projects and groups and example Projects and groups; the Daily ETL and Approval examples show retry_on, Abort, and two questions.
  • LICENSE carries the terms of the bundled Geist fonts; the UI no longer includes code derived from other projects, and the wheel ships no NOTICE.

1.16.0 (2026-09-15)

  • Version bump only; no changes since 1.15.0.

1.15.0 (2026-09-15)

  • Validate credentials in the server with an authenticator, so single sign-on no longer needs a gateway in front of cereyan. @app.authenticator registers one function per process that takes a credential and returns the user's name, or None to reject it. It runs only when enable_auth is true (--enable-auth, CEREYAN_ENABLE_AUTH, app.serve(enable_auth=), or [server] enable_auth; default false), so the same code runs open on a developer's machine, and a server with enable_auth and no authenticator, for instance because its module failed to import, refuses to start. The credential is the Authorization: Bearer header, else the cookie named by auth_cookie; a request with neither is rejected without calling Python. The static token is still checked first, so engines never reach the authenticator, and with auth enabled and no token configured the server generates one for its engines at each start. A run started through the API, the UI, or MCP by a signed-in user records created_by as user:<name>. auth_scope = "all" also covers the UI and custom routes outside /api/, and login_url puts a sign-in link on 401 responses and in the UI, which shows a not-signed-in panel instead of the token prompt.
  • With a token set, an unauthenticated request to a path under /api/ that does not exist answers 401 instead of 404, so the API's routes cannot be probed without a credential.
  • Name an installation: [ui] title in cereyan.toml replaces "cereyan" next to the mark in the top bar and as the browser tab title. Several servers open side by side were indistinguishable. Set the title in the file or in the new Interface card on the Settings page, which saves it through PATCH /api/settings without a restart; an empty title restores cereyan. /api/server and /api/settings report title, and the server writes it into the page's <title>, so the tab shows it before the app loads and on the sign-in screens. A title is trimmed and may have at most 80 characters and no control characters; the top bar cuts a long one to 240 px with an ellipsis and shows it in full on hover.
  • See where every setting came from and what engines inherit, and clean up the store, from the Settings page. Settings now has three tabs, the one open kept in the URL. Environment lists every setting in effect with its source (a flag, a CEREYAN_* variable, app.serve(), cereyan.toml, an edit in Settings, or the default), shows cereyan.toml and the server's process environment with secret values hidden by the server, and reports the Python interpreter and platform (GET /api/settings/environment). Data lists the projects in the store and removes one this server does not serve, with its flows, runs, schedules, backfills, and events (GET /api/projects, GET and DELETE /api/projects/{name}; 409 while the project is served or has a run in progress), and resets the database (POST /api/database/reset with scope history or everything), copying it to <home>/backups/ first unless asked not to. A reset cancels runs in progress, answers 503 to run creation while it runs, keeps what the served code registered, cereyan.toml, saved settings, and the secret key, and starts schedules again from now without catching up; the live stream announces it with database.reset. Migration 0009 indexes events by flow and fills in the flow of older events that named only a run.
  • Use a wide screen: the top bar and every page share one frame, and the Events and Dashboard pages use the width. Pages sat in a centred column at most 1280 px wide under a top bar that spanned the window, so on a wide screen nothing lined up with the brand mark and half the screen was empty. The top bar's row and every page now share a frame up to 1920 px wide, centred, with a gutter that grows from 24 px to 40 px, so the mark and each page title start at the same edge. List pages fill the frame, Settings keeps to a 1080 px column on the left, and a run's page runs edge to edge with its header on the frame. The Events feed fills the page, with a row for each day, a dot in the colour of the state an event's name ends in, and the event's flow and payload; from 1440 px its detail panel stays open beside the feed. The Dashboard adds Recently completed, the eight runs in the range that finished last; from 1680 px Needs attention, Running now, and Upcoming sit side by side above it. The histogram, which stopped stretching past about 1200 px, fills its card, with 24 bars, or 48 from 1680 px.
  • serve() and app.serve() run on a thread other than the main thread. They install their stop-signal handlers only on the main thread; elsewhere, calling stop() on the server passed to ready= shuts it down and serve returns.

1.14.0 (2026-09-14)

  • Serve under a base path, so a reverse proxy can put cereyan at a sub-path. --base-path /cereyan, CEREYAN_BASE_PATH, app.serve(base_path=) or [server] base_path serves everything the TCP listener answers under that path: the UI, its assets, the API, /mcp, and custom routes, which keep the paths they were registered with, so @app.get("/webhook") answers at /cereyan/webhook. / and /cereyan redirect to /cereyan/, and any other path outside it answers 404. server.json records base_path and its url includes it, so the CLI, the Python client, engines and cereyan mcp find the server without being told; /api/server reports both, and the Settings page shows the base path. The Unix socket keeps serving at the root. The proxy must pass the path through unchanged rather than strip it.
  • Breaking: custom routes outside /api/ no longer require the API token. The token check covered every route the router matched, so with a token set a route such as @app.get("/health") answered 401, although the custom routes guide says routes outside /api/ are open. The check now covers /api/* except /api/health, and /mcp. A route that relied on the token has to move under /api/.
  • app.serve(host=, port=, token=, socket=) passed its arguments as the command-line flags, so it ranked above CEREYAN_HOST, CEREYAN_PORT, CEREYAN_TOKEN and CEREYAN_SOCKET. It now ranks below the environment and above cereyan.toml, as documented.
  • cereyan mcp --url kept only the host and port of the URL and posted to /mcp at the root; it now posts to /mcp under the URL's path.
  • A custom route on /api/vocabulary, /api/runs/{id}/resume, /api/runs/{id}/input, /api/rules/{id}/expectations, /api/schedules/{sid}/skips, /api/schedules/{sid}/skips/{fire} or /mcp passed the startup collision check, whose hand-kept list predated those routes, and then collided inside the router. It now fails with the collision error, and a test fails when a built-in route is registered without an entry in the list.
  • /api/stream sends X-Accel-Buffering: no, so nginx does not hold live updates back.

1.13.1 (2026-09-14)

  • An offline flow's timeout_seconds now works on Windows, and a cancel interrupts a sleeping flow there. Both stop a flow from inside its own process, and both assumed a Unix signal. The offline timeout armed only where signal.setitimer exists, which it never does on Windows, so a flow run there by python pipeline.py or cereyan run ran unbounded. A cancel fell back to _thread.interrupt_main(), which only sets the flag CPython checks between bytecodes; a flow in time.sleep never reaches one, so the run ended only when the supervisor terminated the engine after the 10 second grace period, without running its on_cancellation hooks. Both now call signal.raise_signal(SIGINT) from a thread, which goes through CPython's C handler and on Windows also wakes a main thread in time.sleep; a real Ctrl-C during an offline flow is still a KeyboardInterrupt. Windows still cannot interrupt most blocking I/O: an offline timeout lands when the call returns, and a served cancel still ends the engine. An offline flow with a timeout called off the main thread, where no timeout can interrupt it on any platform, now logs a warning instead of running without one silently.

1.13.0 (2026-09-13)

  • A flow disabled by disable_after now resumes after a restart. The resume at the end of the window was a timer held only in the server's memory, and start never read the paused_until it had stored beside it, so a server restarted inside the window left the flow's schedules paused until someone resumed them by hand. Start now derives the window from paused_until: one still open is armed again and ends on time, and one that ended while the server was down resumes as start completes, recording flow.enabled either way. Fires inside the window are still never caught up; fires after it, missed only because the server was down, follow the schedule's catchup policy.
  • Skip upcoming runs, and reschedule from the Flows page. Pausing was the only way to stop a scheduled run, and it dropped every upcoming run until someone remembered to resume. A fire can now be skipped on its own: from the Flows row menu (Skip next run, Skip runs…), the flow page's Skip next… and Upcoming tab, or POST /api/schedules/{id}/skips with fires or next. A skip is stored against the schedule and the fire time, so it survives a restart, a pause and resume, and an edit that keeps the time; catch-up never recreates a skipped fire, and the look-ahead keeps three runs that will start past the skipped ones. At its time the run ends Skipped with reason user without starting, and the runs of flows declared after= it, keyed fan-in included, are created Skipped with reason upstream; every other Skipped run still triggers its downstream as before. Undo, or DELETE /api/schedules/{id}/skips/{fire}, takes a skip back until its time. GET /api/flows/{id}/upcoming marks each run skipped, with skipped_by and skipped_at, and, with projected=N, also lists fires past the look-ahead; each schedule reports skipped, and its next_fire is the next fire that will run. Reschedule… edits a cron schedule as a daily, weekly or monthly time in a timezone, or as raw cron, with its catch-up options and the coming week before and after the change; skips an edit no longer produces are dropped and recorded as schedule.skips_dropped. run.skipped now carries reason.
  • Breaking: a schedule edit lasts until the next restart unless it asks to persist. PATCH /api/schedules/{id} used to mark the row persist, which detached a code-declared schedule from its declaration for good and said so only afterwards, in small grey text. It now leaves persist as it was unless the body sets it, so an edit from the flow page, the Reschedule dialog, or MCP's edit_schedule holds until the server restarts and the declaration applies again. Send persist: true for the old behaviour.
  • CI runs the documentation tests on Windows, so every suite now runs on Linux, macOS and Windows. The Python suite has run there since 1.5.0, once two things were fixed: a test liveness probe that killed the process it was asking about, and a stop path that hard-killed the server before it could shut down (the 1.5.0 graceful-stop entry). The documentation step was the last one conditional on the platform. No product behaviour changed.

1.12.0 (2026-09-10)

  • A rule that could never fire is now refused instead of stored. @app.rule(on="run.failure") registered cleanly, stored cleanly, and never fired — no error, no warning, no log line — because nothing on either path checked an event name. The names themselves lived in four places that drifted apart: literals across the server crate, a smaller set on the offline path, a hand-written reference table that claimed to mirror them, and a free-text box in the rule form. There is now one catalogue, in crates/core/src/events.rs, that the emit sites are compiled against, so renaming an event breaks the build. The engine owns the prefixes run., task_run., flow., schedule., resource., rule. and expectation.: a name under one of them that nothing emits is rejected at import by @app.rule, with 422 by the rules API, and by emit_event, each with the nearest real name — run.failure answers "did you mean run.failed?". Every name outside those prefixes is a custom event and is not checked, so on="orders.table_empty" still needs no registration. cereyan.events and cereyan.states expose the catalogue as str constants that are the wire names, so events.run.failed and "run.failed" are interchangeable and nothing downstream can tell them apart; events.run.any is "run.*". GET /api/vocabulary serves the same list, the rule form offers it as suggestions while still taking a typed custom name, and docs/reference/events.md is generated from it.

  • A rule naming a state type now matches that type's sub-states. states= was documented as "state types the run must be in" and matched the sub-state name, so states=["Scheduled"] matched no run that was Late, AwaitingRetry or AwaitingResource, and expressing "any scheduled run" meant listing all four. A value now matches the run's state type or its sub-state name: ["Scheduled"] covers the sub-states, ["Late"] still narrows to one. This widens what an existing rule matches — it can only match more, never less, since no type and sub-state name collide — so a rule written to exploit the old narrow reading fires more often. The guards (once, cooldown_seconds, max_per_minute) bound the effect.

  • @app.rule rejects an unrecognised keyword instead of dropping it, so cooldownseconds=60 no longer leaves a rule running on defaults nobody asked for. once="never", valid since rules shipped and documented nowhere, is on Events and rules and the rules guide. The Rules page marks a rule that has never fired, which is the one failure that name checking cannot catch: a rule spelled plausibly that matches nothing. CodeRule.extra_actions, which nothing set and no decorator accepted, is gone.

  • The schedule drift test no longer gates a release on a wall-clock ceiling. test_schedule_fires_on_time_with_low_drift asserted that a run starts within 250 ms of its scheduled time; a Windows runner measured 294 ms and failed the 1.11.0 build, which is the flake the repository already has a rule against — the correctness gate excludes wall-clock ceilings, because a bound calibrated on a developer machine says nothing about correctness on a shared runner. The 250 ms was also a loosened stand-in for a target that already has a home: benches/e2e.py measures schedule_drift_ms against the documented 50 ms, with a per-platform baseline and regression detection. The test is now test_schedule_fires_at_its_scheduled_time and asserts what does not depend on the hardware — the run is materialised at the anchor, it completes, and it does not fire early. No product behaviour changed.

1.11.0 (2026-09-09)

  • Documentation: @flow(group=...) shipped in 1.10.0 with a paragraph on App and projects and nothing a reader could copy. It now has a section of its own there with a snippet, and Flows and parameters lists it among what a flow declares, so the option is findable from the page a reader reaches for when choosing one. The README's screenshot captions describe the collapsible groups the recaptured Flows and Runs shots already show.

1.10.0 (2026-09-09)

  • Flows can declare a group, and the UI collapses by it. @flow(group="nightly") names the group a flow is listed under; without it a flow is grouped under its project, as before. Groups are a flat axis rather than a level inside the project, so flows in different projects declaring the same group form one group and a group named after a project merges with the flows defaulting to it — which is how a flow joins a group it does not live beside. A run carries its flow's group, read through the flow rather than stored on the run, so renaming a group moves the run history with it instead of cleaving it in two. Both the Flows and the Runs page now draw their groups as collapsible sections whose header is the same columns rolled up: the soonest next fire, the group's recent runs, its states as a bar with counts, the tag union, and the row count. Flows the running server no longer has registered are counted separately as stale, because a group whose flows all last succeeded but have since deregistered would otherwise read as healthy behind a folded header. A group of more than five rows starts collapsed, a lone group is always open, a search opens every group it matches, and the default is fixed when a group is first seen so a live update never shuts one under the cursor.

  • An engine still reporting a run it has just finished keeps retrying for up to ten minutes when its server disappears, so a restarted server records the outcome instead of seeing a crash. That is deliberate, and it is now on Engines and the home directory, where the thirty to forty second figure for an idle engine had been the only number given. Three CI failures came from tests that did not know it: wait_run returns when the server records the terminal state, while the engine is still flushing its last events and log lines. Two shutdown tests killed the server inside that window and measured the ten minute path; the retention test rewrote the timestamps of log rows the engine was still adding to, so a line landing afterwards carried a fresh one and was rightly never swept. ServerProcess.wait_idle gives all three the wait they were missing.

1.9.2 (2026-09-09)

  • A clock-armed proactive rule no longer cries wolf on its first tick. unless with at fires when an expected event did not arrive in the look-back window, and an early tick's window reaches back past the rule's own creation — where the store is empty because nothing had happened yet, not because anything was missed. A rule with within=3 fired 0.77 seconds after the server started, and one saved from the UI could page someone about the hour before it existed. A tick now evaluates only once the rule has been watching for a whole window; the schedule keeps running meanwhile, so the first honest lapse is delayed by at most within. This was recorded in WINDOWS.md as an unexplained Windows-only flake; it is neither. Windows lost the coin flip more often because a slower start delays the first event, and CI caught it on Linux at 1.9.1. The xfail marker that blamed Windows is gone, and test_clock_armed_rule_waits_until_it_has_watched_a_whole_window pins the behaviour.

1.9.1 (2026-09-09)

  • The engine give-up test no longer gates a release on a wall-clock ceiling. test_idle_engine_gives_up_when_the_server_is_killed asserted that a SIGKILLed server's engine exits within 60 seconds, against a bound of 40; on a loaded macOS runner the same commit that passed on main failed on the tag, which is the flake the repository already has a rule against. It now asserts only that the engine gives up, and the 40 second bound moved to a performance test that just bench runs on calibrated hardware, where a number like that means something. No product behaviour changed.

1.9.0 (2026-09-09)

  • Documentation: features that shipped without a page. The MCP guide still described fifteen tools and named none of the four schedule tools added in 1.8.0. cereyan.runtime, the public view of the run in progress (run, task_run, flow), was exported from the package and mentioned nowhere; it now has a section on the Python API page and a pointer from Runs and states. The events reference listed five of the fifteen kinds the SSE stream carries and the OpenAPI description behind GET /api/stream listed four; both now list all of them with their payloads. The tour still said the run page's Details tab shows an exit code, which was removed in 1.4.0, and did not mention the task run page.
  • Documentation: the three Windows behaviours tracked in WINDOWS.md are now stated where a reader meets them rather than only in a file that ships with the repository. A flow's timeout_seconds does nothing on the offline path there, so Retry, time out and survive crashes says so; a cancel cannot interrupt a flow blocked in a call, so Engines and the home directory says so; a clock-armed proactive rule can lapse while its events are still arriving, so Detect when something did not happen says so. Install carries the three as a table and Design and limitations names them.

1.8.0 (2026-09-09)

  • An agent can manage schedules. pause_schedule and resume_schedule both took a schedule_id that no MCP tool returned, so reaching them meant already knowing the id: a schedule that had never fired was invisible while the tool to pause it sat in the list. list_schedules closes that, filtered by flow or project, reporting each schedule's spec, whether it is active, when it next fires, and whether it came from code, the interface or an agent. create_schedule, edit_schedule and delete_schedule complete the lifecycle; create and edit return the next few fire times so a spec can be checked before it fires unattended, and schedules an agent creates record source: "mcp". Two guards: editing a schedule declared in a flow's code returns a note that the declaration no longer governs it, and deleting one is refused, because startup recreates it from the declaration and a success would be untrue. Flows still run on demand by default; a flow needs no schedule.

1.7.0 (2026-09-09)

  • Cereyan is production ready. The Python API, the HTTP API, the CLI, the MCP surface and the database schema are settled; the under-development warning is gone from the README and the documentation, and the PyPI classifier moves from Beta to Production/Stable. The README also said the package was not yet published, which stopped being true at 1.4.0.

  • Benchmarks for the write path a served task run actually travels. transition on both tables and apply_report over the event sequence an engine child sends had no benchmark at all, so a change to the store showed up only once it was large enough to move an end-to-end number that also contains subprocess spawn, HTTP and the client's flush interval. cargo bench -p cereyan-store --bench hot_path now measures both directly, at three batch widths so per-batch cost that grows faster than the batch is visible as a shape. A second bench sizes the stream payload built for every transition. just bench gains task_run_cost_us, the orchestration cost of one completed task run, with a 200 µs target — the unit a fan-out workload is actually counted in.

  • The performance baseline is refreshed. It was last written at 1.3.0 and three of its numbers had drifted, schedule_drift_ms past the 20 percent gate. No code explains it: every file on the ingestion and dispatch paths — writer.rs, read.rs, dispatch.rs, api/engine.rs, index.rs, scheduler.rs, timer.rs, and the core state rules — is byte-identical to that commit, and Cargo.lock differs only in the version string. The repository pins no Rust toolchain, so a baseline does not survive a compiler upgrade; the numbers here were recorded on rustc 1.92.0. Every target is still met with room to spare.

1.6.1 (2026-09-09)

  • Internal refactoring.

1.6.0 (2026-09-09)

  • @flow and @task reject async def. An async body was never awaited: the call returned a coroutine, the body never ran, and the run was recorded Completed — a pipeline that fetched nothing and reported success, its only trace a RuntimeWarning on stderr after the fact. Both decorators now raise TypeError at decoration, naming the function and showing the synchronous wrapper. async def with yield is rejected too: inspect.iscoroutinefunction is false for an async generator function, which failed the same way. Async bodies remain unsupported; async def route handlers are unaffected. Anything this breaks was already reporting success without running.
  • A guide for fetching from an HTTP API: building the client once so a warm engine reuses its connection pool, map over a ThreadRunner for concurrency, and where the reuse stops (isolated=True, ProcessRunner, offline runs). That an engine imports its module once, and so shares module-level state across the runs it serves, is now stated on Engines and the home directory and covered by a test, rather than being an undocumented accident of the implementation.

1.5.0 (2026-09-09)

  • The runtime home is readable only by the account that created it. It was created with whatever the umask gave — 0755 on a typical machine — so db.sqlite, with every run, log, event and variable, was readable by any other account, and secret.key was too: it was narrowed to 0600 after being written, leaving a window in which the key that decrypts every secret was world-readable, and that narrowing never ran on Windows at all. Protecting the directory covers everything in it, closes the window, and needs nothing platform-specific for a home under your user profile. A home from an earlier version is narrowed when opened, with a message saying so.
  • An engine whose server disappeared could take up to ninety seconds to exit rather than the thirty its documentation promised. It notices only between requests, so the wait was bounded by the client's long-poll timeout — ninety seconds — and not by the thirty second idle threshold that appeared to govern it. The timeout is now forty seconds, ten more than the server ever holds a request, so the worst case is forty rather than ninety. The regression test used to wait exactly ninety seconds and so raced the very timeout that defeated the bound; it now waits sixty against a forty second bound.
  • Windows: stopping the server no longer kills the engine executing a run. Engine children were spawned in their own process group on Unix — so a Ctrl-C or a stop aimed at the server never reached them — but there was no Windows equivalent, so a console control event swept up every engine, including the one mid-run that a restarted server is supposed to adopt. A run in flight when the server stopped was lost rather than resumed. Engines now get their own group on both platforms.
  • server.json records a URL clients can actually use. It carried the address the listener bound, so starting the server on every interface wrote http://0.0.0.0:<port> — a bind address, not a destination. Linux and macOS route that to loopback, so it worked by accident; Windows refuses it, and nothing that reads the discovery file could find the server there. url now names loopback when the bind address is unspecified, and host still records what was bound.
  • Windows: cereyan serve can be stopped gracefully. It handled SIGTERM, which Windows never delivers, so anything stopping the server other than an interactive Ctrl-C — a service manager, a script, a supervisor — ended the process before it could shut down: the discovery file was left behind, pending writes were not flushed, the WAL was not checkpointed, and engine processes were orphaned. A console control event arrives as SIGBREAK on Windows and is now handled the same way SIGTERM is on Unix. Unix behaviour is unchanged. Found by running the Python test suite on Windows for the first time.

1.4.0 (2026-09-08)

  • The Intel macOS wheel is cross-built from the arm64 runner. GitHub retired the macos-13 label, so that build queued forever and, because the smoke test and the PyPI upload wait for every wheel, no release could complete at all.
  • cargo bench runs again. Criterion's flags were being handed to the auto libtest harness of every lib target, which rejects them, so the benchmarks stopped at the first target reached — through just bench as much as in CI. Each crate's lib now sets bench = false.
  • Wall-clock ceilings moved out of the test suite that gates a release. Two Rust timing tests are #[ignore] and the Python performance marker is deselected there; both run in just bench and in CI's benchmark job, which is where the per-platform baselines live. A ceiling calibrated on a developer machine failing on a shared CI runner said nothing about correctness. benches/e2e.py --no-ceilings reports an absolute miss instead of failing, while a regression against a baseline still fails.
  • Windows: cereyan-server now compiles. The Unix socket listener's serve path was never excluded on platforms without Unix sockets, so the crate failed to build and the Windows wheel and test job have been broken since the socket landed in 1.1. Windows had no working wheel for 1.1, 1.2, or 1.3 despite being listed as a supported platform.
  • Engines now end with the server. A graceful stop answers every waiting engine with an instruction to exit and signals any that were idle between requests, so cereyan serve leaves no engine child behind; an engine executing a run is deliberately left alone, so a restarted server still adopts its run. An engine that loses its server without being told to stop — a kill, a crash — exits by itself after thirty seconds of failing to reach it. This reverses a documented guarantee: the server previously exited without terminating any engine process, so anything that relied on the warm pool outliving a graceful stop or restart now sees a cold pool instead.
  • Removed exit_code from runs. The field was in the Run model, the OpenAPI and MCP surfaces, and the run page, but nothing ever wrote it, so it was always null; the store's set_run_exit write path had no callers. Clients reading run.exit_code should drop it. The SQLite column stays, unread, because migrations are append-only.
  • Packaging: the wheel and sdist now carry LICENSE and NOTICE, project URLs, keywords, an author, and a fuller classifier set, so the PyPI page renders its links, licence, and images. Tagging a release now publishes to PyPI from CI through trusted publishing, which the release checklist already promised.

  • UI redesign: a top bar replaces the sidebar and breadcrumb bar, with the eight sections as tabs, a project switcher that scopes every list, a ⌘K palette that jumps to sections, flows, runs, and artifacts, and a warm neutral palette in light and dark with Geist bundled. Controls are shadcn components. The dashboard leads with a Needs attention list (paused, failed, crashed, and late runs with inline actions), Running now with task progress, and the histogram with an axis; the runs page has popover filters, a task-state bar per run, and a floating selection bar; the run page is a workbench with a tasks rail that filters the logs to one task run and shows retry countdowns; the flows page groups by project with the schedule in words, a run-history sparkline, and a Dependencies panel of after= chains and fan-in groups.

  • API: run list items and GET /api/runs/{id} carry task_counts by task state; recent_runs entries on flows gain the run's duration; AwaitingRetry details record retries.

  • Documentation authoring guide: docs/AGENTS.md now exists, covering the layout, the generated pages and their generators, the page kinds and section budgets, the vocabulary, the style rules, the tested-block markers and fixtures, the redirect rule, and the commands. just lint fails when it is missing or stops naming a generated page, which is what let the documentation-restructure entry below promise a file that was never written.

  • The MCP reference is generated: scripts/gen_mcp_reference.py renders docs/reference/mcp.md from tests/mcp_snapshot.json, a snapshot of the server's own handshake, tools, resource templates, prompts, and response keys, checked in just lint like the CLI and HTTP references. The page now carries the protocol version, the instructions the server gives the model, each argument's type, default, and range, the keys every tool returns (including next_cursor and verdict), the fields of the lists they hold, and the prompt text.
  • Documentation: cereyan mcp --token never worked; --token is global and goes before the subcommand. The agent guide says so, and calls the two resources templates, which is what they are.

  • Overlap soak (just soak, benches/soak_overlap.py): fifteen scheduled flows across enqueue, skip, and cancel_new run for an hour against a real server and are checked against nine overlap invariants; --quick for twelve minutes, --keep to browse the UI afterwards, manual workflow_dispatch job in CI.

  • Documentation restructured into Get started, Concepts, Guides, Reference, Examples, and Design on a Material for MkDocs site with llms.txt output; every Python block in the docs and every file under examples/ is executed by the test suite; the CLI and HTTP API references are generated from the parser and the OpenAPI snapshot; a docstring check covers the public API; docs/AGENTS.md and an OpenSpec rule keep future changes documented. Old page URLs redirect.
  • Docstrings for every public class, function, and method, and help text for every CLI option (cereyan <command> --help). No behaviour changes.
  • Seven literate examples under examples/: quickstart pipeline, daily ETL, fan-in, approval, alerting, webhook route, agent diagnosis.

1.3.0 (2026-09-06)

  • Fan-in dependencies: @flow(after=["a", "b"], batch_key="day") runs the downstream once per key value after every upstream completed it; flow.fan_in events, upstream lists in the flow summary and graph.

1.2.0 (2026-09-06)

  • Built-in MCP server: POST /mcp (Streamable HTTP, JSON replies) and cereyan mcp (stdio proxy) with fifteen curated tools, two resources, and a diagnose_run prompt; runs started by agents record created_by = mcp:<client>.
  • Human-in-the-loop: wait_for_input(prompt, schema=None) pauses a run, POST /api/runs/{id}/resume answers it, the run page shows the question with a form; the engine and resources are released while a run waits. run.paused and run.resumed events.
  • Paused is now entered only from Running.

1.1.0 (2026-09-06)

  • Proactive rules: unless with within (event-armed) or at cron with tz (clock-armed) fires when an expected event does not happen; lapses are expectation.lapsed events, expectations survive restarts, GET /api/rules/{id}/expectations, and an "Unless" section in the rule form.
  • API token: --token, CEREYAN_TOKEN, app.serve(token=), or [server] token protects every API route except health; the UI prompts for it, clients and engines send it, and server.json records auth.
  • Unix socket listener (--socket, CEREYAN_SOCKET, [server] socket) trusted by file permission, recorded in server.json, and used by the Python client.
  • Async custom route handlers on one shared event loop.
  • Artifacts page and GET /api/artifacts with filters, keyset pagination, and per-key history.
  • Negative priority lowers engine niceness on Unix.
  • Downgrading to 1.0.x: delete rules that use unless first; 1.0 does not load them.

1.0.0 (2026-09-06)

First release. One wheel, no runtime dependencies, Python 3.11 or newer.

  • Flows and tasks with parameters from type hints, offline execution into a local SQLite store, and the cereyan run and cereyan runs ls commands.
  • cereyan serve: HTTP API with an OpenAPI document, server-sent events, a warm pool of engine processes that survive server restarts, custom routes, and the embedded React UI.
  • One runtime home per machine (--home, CEREYAN_HOME, ~/.cereyan); flows identified by project and name; cross-project handoff from scripts to a running server.
  • Schedules (cron, interval, RRule) with timezones and catch-up policies; retries, timeouts, hooks, and crash chains; Targets with atomic writes; input and source caching; backfills; resources, priority, overlap policies, and disable windows; concurrent tasks with futures, map, thread and process runners; single-upstream flow dependencies; timeline graph.
  • Events, rules with seven actions and templating, artifacts, variables with encrypted secrets, settings and retention, benchmarks against the performance targets, docs, and the release wheel matrix.
  • Performance: engine reports are chunked and accepted up to 64 MB, task-run events are appended in one batch per report, and opening a large database no longer reads the whole file when the previous shutdown was clean.