Corsac Phase 1: From Workflow Sketches to a Real Runtime

First published in my LinkedIn newsletter. Corsac is still a work in progress; this describes it as it was then.
Gust is the language side of this project. It describes a workflow as typed states, transitions, handlers, effects, and actions.
Corsac is the operating layer around that contract. It's the thing that makes a compiled workflow usable by an operator: validate it, build it, run it, inspect what happened, repair stale runs, and eventually package it for a deployment target.
Phase 1 had one job: cross the line from architecture document to actual runtime.
Not a hosted product. Not a visual designer. Not a full workflow platform. The goal was narrower and more useful — prove that compiled Gust workflows can be operated from a local CLI with enough structure around configuration, durability, side effects, and packaging that the next phase has something real to build on.
The short version: Phase 1 is complete for local CLI/runtime use. The main external gap left is a real AWS Lambda smoke test against an actual AWS account. The deploy and invoke commands exist and are locally tested with fake AWS CLI helpers, but I'm not going to claim real cloud validation until I've run the canonical workflow through AWS itself. That's the gap I keep walking past, and I'd rather name it than dress it up.
The wedge
The accepted Phase 1 wedge was deliberately constrained:
- CLI-first
- Lambda-friendly for AWS-heavy teams
- compiled Gust workflows
- node-boundary durability
- explicit
effectversusactionsemantics - no visual designer
- no hosted control plane
That constraint mattered. Workflow tools sprawl fast.
It's tempting to jump straight to dashboards, collaboration, hosted run storage, trigger management, and a marketplace of handlers. Those might become useful later. None of them answer the first question: can a typed workflow be compiled and operated reliably from a developer machine?
Phase 1 focused on that question.
What landed
The CLI now has the core local workflow lifecycle:
corsac check docs/examples/workflows/order-notification.gu
corsac build docs/examples/workflows/order-notification.gu
corsac run docs/examples/workflows/order-notification.gu \
--payload @docs/examples/payloads/success.json
corsac history
corsac show-run <execution-id>
corsac repair-runs
That path is backed by a real runtime boundary instead of a one-off demo command. There's an app layer, executor layer, local run store, runtime configuration loader, workflow linter, Lambda packaging code, and a test suite around the pieces most likely to regress.
The canonical example is an order notification workflow.
Intentionally ordinary: receive a webhook-shaped payload, parse an order, format a message, call a Slack-like action. That kind of boring example is useful because it forces the runtime to deal with the things production automation actually cares about — payload shape, handler binding, credentials, side effects, failure records, repeatable execution.
It also resists demo inflation. If the runtime can't make a webhook-to-notification workflow inspectable and repeatable, it has no business growing a larger product surface yet.
Configuration became real
Early versions of Corsac were wired around local defaults. That was fine while proving the executor, but it wasn't enough for a product-shaped runtime. Phase 1 added corsac.toml as the project configuration file.
The config model is intentionally small:
- named environments
- handler bindings
- env-var-backed credentials
- strict validation
- sanitized config staging for packaged deployments
One important cleanup: moving away from plaintext deploy-time secrets. The Lambda deploy path uses --lambda-env-from KEY=ENV_VAR instead of --lambda-env KEY=value, so secrets come from the caller's environment and pass through the AWS CLI without baking secret values into command history or docs examples.
The config migration also moved from an earlier YAML shape to TOML. Not glamorous. The kind of choice that matters anyway for a CLI tool: the checked-in config is easier to read, and strict unknown-field rejection keeps typos from silently changing runtime behavior.
Side effects needed a runtime rule
The most important semantic distinction in Corsac Phase 1 is effect versus action.
The language distinction comes from Gust. Corsac needs it for a runtime reason.
The rough model in Phase 1:
- An
effectcan do external work that's safe to treat as non-committing or repeatable in the workflow path. - An
actionis the committed side-effectful step that should happen at most once in a handler path and should appear at the boundary where the workflow transitions onward.
This isn't just vocabulary. It's part of the durability model.
Corsac records execution progress at node boundaries. If a run is interrupted, the runtime can reason about the last completed node and repair stale records. But that model only works if the workflow is honest about where committed side effects happen.
Phase 1 therefore added conservative linting around effect and action usage. Today that linting is source-level enforcement, not parser-backed proof from Gust metadata. It can be confused by comments, strings, quoted braces, or future Gust syntax. That's acceptable for Phase 1 because the limitation is explicit and documented. Hidden limitations are the dangerous kind.
The deeper design point deserves its own post (and gets one — see post 5). For Phase 1, the important result is that the runtime now enforces the side-effect rule it depends on instead of treating it as a comment.
Run records and repair
A workflow runtime without inspection is just a black box with a nicer command name.
Corsac now stores local execution records under .corsac/runs/. The records include status, failure classification, node progress, and event data sufficient for local inspection and repair. The CLI can list history, show an individual run, and repair runs that were left stale by interruption.
Some less visible hardening landed here:
- atomic temp-and-rename record writes
- synchronized event repair records
- per-run repair locks
- child process cleanup on protocol errors
- stderr draining before returning executor failures
None of that's flashy. It's the difference between "the demo worked once" and "I can keep using this while I build the rest of the system."
Packaging for Lambda
The Phase 1 product wedge points toward Lambda-heavy AWS teams, so local execution alone wasn't enough. Corsac now has commands for Lambda packaging, deployment, and invocation:
corsac package lambda docs/examples/workflows/order-notification.gu --use-docker
corsac deploy lambda docs/examples/workflows/order-notification.gu \
--function-name <name> \
--role <role-arn> \
--region <region> \
--lambda-env-from CORSAC_SLACK_TOKEN=CORSAC_SLACK_TOKEN \
--yes
corsac invoke lambda <function-name> \
--region <region> \
--payload @docs/examples/payloads/success.json
The Windows-friendly path uses Docker to produce a Linux/x86_64 Lambda package. On Linux, the native toolchain path can be used directly. The package zip is written through a temporary path and renamed into place to avoid leaving corrupt artifacts after interrupted builds.
The deploy path also got stricter during release-readiness work. It requires AWS caller identity discovery and explicit --yes confirmation before mutating Lambda resources. It waits between Lambda update operations and uses AWS CLI file:// payloads for environment updates.
The caveat is important: this path is implemented and covered with fake AWS CLI helpers, but the real AWS smoke test is still pending.
I'm treating that as external validation, not as a reason to keep expanding Phase 1 indefinitely. The temptation is real — there's always one more polish item — but the smoke test is the actual unblock.
CI and the Gust fixture choice
The canonical workflow is now a release-gate acceptance fixture. CI exercises the local check -> build -> run path, including success and failure payloads.
One deliberate choice: the acceptance test uses a pinned generated Rust fixture for the Gust output instead of invoking a freshly installed gust binary in the default CI path.
That tradeoff keeps CI stable while Gust and Corsac are evolving together. The fixture has update instructions, and the backlog includes a live Gust compatibility gate for release validation. For now, the fast test proves Corsac against the pinned compiler output. The live compiler test can become a separate release or manual gate.
This is a good example of the kind of honesty I want in this project. The fixture-backed test is valuable. It isn't the same as proving compatibility with every future Gust change. Saying so is part of the work.
What I'm not claiming yet
Phase 1 has a real runtime now, but there are limits I want to keep visible:
- The AWS smoke test hasn't run against a real account yet.
- The
effectandactionlinter is source-level, not parser-backed. - Execution history is local filesystem only.
- There's no multi-environment promotion workflow.
- Windows Lambda packaging requires Docker.
- Full event-sourced replay is deferred.
- There's no hosted control plane or visual designer.
Some of these are gaps. Some are intentional boundaries. The distinction matters.
For example, local filesystem history isn't a mistake. It's the right Phase 1 choice because it keeps the runtime inspectable without forcing a database or service dependency too early. If Corsac becomes useful for teams, shared history will become a real product requirement. Today it isn't one yet.
What Phase 2 is likely about
The Phase 2 backlog is clearer because Phase 1 forced the rough edges into the open.
The likely next items:
- run the real AWS smoke test
- add a live Gust compatibility gate
- make
corsac show-rundefault to the latest run when no ID is provided - improve machine-readable history and inspection output
- move
effectandactionvalidation closer to Gust metadata or a parser - improve generated artifact metadata
- make payload fixtures first-class in docs
- explore shared execution history
The deeper question for Phase 2 is where Corsac should stop being a local runtime and start becoming team infrastructure. I don't want to answer that too early. The next phase should be driven by the pressure that shows up while using the CLI for real workflows — not by what would look impressive in a roadmap.
The current verdict
Phase 1 did what it needed to do. Corsac is no longer just an architecture sketch around Gust. It's a working local runtime with a CLI, configuration model, run records, repair path, release docs, tests, and a Lambda packaging/deploy story that's ready for real AWS validation.
That's a useful checkpoint.
The next step isn't to pretend the system is bigger than it is.
The next step is to use it, close the AWS smoke gap, and let the Phase 2 backlog be shaped by actual friction.





