Changelog¶
Versions follow the wheel on PyPI. Each entry lists what changed and, where a downgrade needs care, how to do it.
Changelog¶
Unreleased¶
1.13.0 (2026-09-13)¶
- A flow disabled by
disable_afternow 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 thepaused_untilit 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 frompaused_until: one still open is armed again and ends on time, and one that ended while the server was down resumes as start completes, recordingflow.enabledeither way. Fires inside the window are still never caught up; fires after it, missed only because the server was down, follow the schedule'scatchuppolicy. - 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}/skipswithfiresornext. 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 endsSkippedwithreasonuserwithout starting, and the runs of flows declaredafter=it, keyed fan-in included, are createdSkippedwithreasonupstream; every other Skipped run still triggers its downstream as before. Undo, orDELETE /api/schedules/{id}/skips/{fire}, takes a skip back until its time.GET /api/flows/{id}/upcomingmarks each runskipped, withskipped_byandskipped_at, and, withprojected=N, also lists fires past the look-ahead; each schedule reportsskipped, and itsnext_fireis 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 asschedule.skips_dropped.run.skippednow carriesreason. - Breaking: a schedule edit lasts until the next restart unless it asks to persist.
PATCH /api/schedules/{id}used to mark the rowpersist, which detached a code-declared schedule from its declaration for good and said so only afterwards, in small grey text. It now leavespersistas it was unless the body sets it, so an edit from the flow page, the Reschedule dialog, or MCP'sedit_scheduleholds until the server restarts and the declaration applies again. Sendpersist: truefor 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, incrates/core/src/events.rs, that the emit sites are compiled against, so renaming an event breaks the build. The engine owns the prefixesrun.,task_run.,flow.,schedule.,resource.,rule.andexpectation.: a name under one of them that nothing emits is rejected at import by@app.rule, with 422 by the rules API, and byemit_event, each with the nearest real name —run.failureanswers "did you meanrun.failed?". Every name outside those prefixes is a custom event and is not checked, soon="orders.table_empty"still needs no registration.cereyan.eventsandcereyan.statesexpose the catalogue asstrconstants that are the wire names, soevents.run.failedand"run.failed"are interchangeable and nothing downstream can tell them apart;events.run.anyis"run.*".GET /api/vocabularyserves the same list, the rule form offers it as suggestions while still taking a typed custom name, anddocs/reference/events.mdis 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, sostates=["Scheduled"]matched no run that wasLate,AwaitingRetryorAwaitingResource, 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.rulerejects an unrecognised keyword instead of dropping it, socooldownseconds=60no 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_driftasserted 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.pymeasuresschedule_drift_msagainst the documented 50 ms, with a per-platform baseline and regression detection. The test is nowtest_schedule_fires_at_its_scheduled_timeand 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_runreturns 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_idlegives 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.
unlesswithatfires 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 withwithin=3fired 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 mostwithin. This was recorded inWINDOWS.mdas 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. Thexfailmarker that blamed Windows is gone, andtest_clock_armed_rule_waits_until_it_has_watched_a_whole_windowpins 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_killedasserted 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 onmainfailed 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 aperformancetest thatjust benchruns 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 behindGET /api/streamlisted 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.mdare now stated where a reader meets them rather than only in a file that ships with the repository. A flow'stimeout_secondsdoes 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_scheduleandresume_scheduleboth took aschedule_idthat 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_schedulescloses 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_scheduleanddelete_schedulecomplete 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 recordsource: "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.
transitionon both tables andapply_reportover 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_pathnow 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 benchgainstask_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_mspast 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, andCargo.lockdiffers 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)¶
@flowand@taskrejectasync def. An async body was never awaited: the call returned a coroutine, the body never ran, and the run was recordedCompleted— a pipeline that fetched nothing and reported success, its only trace aRuntimeWarningon stderr after the fact. Both decorators now raiseTypeErrorat decoration, naming the function and showing the synchronous wrapper.async defwithyieldis rejected too:inspect.iscoroutinefunctionis false for an async generator function, which failed the same way. Async bodies remain unsupported;async defroute 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,
mapover aThreadRunnerfor 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 —
0755on a typical machine — sodb.sqlite, with every run, log, event and variable, was readable by any other account, andsecret.keywas too: it was narrowed to0600after 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.jsonrecords a URL clients can actually use. It carried the address the listener bound, so starting the server on every interface wrotehttp://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.urlnow names loopback when the bind address is unspecified, andhoststill records what was bound.- Windows:
cereyan servecan be stopped gracefully. It handledSIGTERM, 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 asSIGBREAKon Windows and is now handled the same waySIGTERMis 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-13label, 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 benchruns 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 — throughjust benchas much as in CI. Each crate's lib now setsbench = false.- Wall-clock ceilings moved out of the test suite that gates a release. Two Rust timing tests are
#[ignore]and the Pythonperformancemarker is deselected there; both run injust benchand 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-ceilingsreports an absolute miss instead of failing, while a regression against a baseline still fails. - Windows:
cereyan-servernow 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 serveleaves 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_codefrom runs. The field was in theRunmodel, the OpenAPI and MCP surfaces, and the run page, but nothing ever wrote it, so it was always null; the store'sset_run_exitwrite path had no callers. Clients readingrun.exit_codeshould drop it. The SQLite column stays, unread, because migrations are append-only. -
Packaging: the wheel and sdist now carry
LICENSEandNOTICE, 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}carrytask_countsby task state;recent_runsentries on flows gain the run's duration;AwaitingRetrydetails recordretries. -
Documentation authoring guide:
docs/AGENTS.mdnow 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 lintfails 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.pyrendersdocs/reference/mcp.mdfromtests/mcp_snapshot.json, a snapshot of the server's own handshake, tools, resource templates, prompts, and response keys, checked injust lintlike 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 (includingnext_cursorandverdict), the fields of the lists they hold, and the prompt text. -
Documentation:
cereyan mcp --tokennever worked;--tokenis 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 acrossenqueue,skip, andcancel_newrun for an hour against a real server and are checked against nine overlap invariants;--quickfor twelve minutes,--keepto browse the UI afterwards, manualworkflow_dispatchjob in CI. - Documentation restructured into Get started, Concepts, Guides, Reference, Examples, and Design on a Material for MkDocs site with
llms.txtoutput; every Python block in the docs and every file underexamples/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.mdand 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_inevents, upstream lists in the flow summary and graph.
1.2.0 (2026-09-06)¶
- Built-in MCP server:
POST /mcp(Streamable HTTP, JSON replies) andcereyan mcp(stdio proxy) with fifteen curated tools, two resources, and adiagnose_runprompt; runs started by agents recordcreated_by = mcp:<client>. - Human-in-the-loop:
wait_for_input(prompt, schema=None)pauses a run,POST /api/runs/{id}/resumeanswers it, the run page shows the question with a form; the engine and resources are released while a run waits.run.pausedandrun.resumedevents. Pausedis now entered only fromRunning.
1.1.0 (2026-09-06)¶
- Proactive rules:
unlesswithwithin(event-armed) oratcron withtz(clock-armed) fires when an expected event does not happen; lapses areexpectation.lapsedevents, 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] tokenprotects every API route except health; the UI prompts for it, clients and engines send it, andserver.jsonrecordsauth. - Unix socket listener (
--socket,CEREYAN_SOCKET,[server] socket) trusted by file permission, recorded inserver.json, and used by the Python client. - Async custom route handlers on one shared event loop.
- Artifacts page and
GET /api/artifactswith filters, keyset pagination, and per-key history. - Negative
prioritylowers engine niceness on Unix. - Downgrading to 1.0.x: delete rules that use
unlessfirst; 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 runandcereyan runs lscommands. 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.