Setup is multi-step
Users may provide a name, prompt, trigger, model, repository list, plugin config, or tarball details over time. The service needs a place to persist incomplete work.
Automation drafts design
PR 417 adds server-backed automation drafts so setup UIs can save partial work, validate it incrementally, and test-run complete drafts without letting those drafts participate in normal scheduling or event dispatch.
Users may provide a name, prompt, trigger, model, repository list, plugin config, or tarball details over time. The service needs a place to persist incomplete work.
automation_runs.automation_id is non-null, and the
dispatcher reads executable fields from Automation. A draft
cannot dispatch directly unless it first has a materialized automation
row.
A user-disabled automation and a materialized draft are both not
scheduled. But they mean different things, so a boolean
enabled flag is not expressive enough on its own.
Problem
The setup experience needs server-backed drafts that can be saved before they are valid automation creation requests. At the same time, the backend must preserve its existing invariants: schedulers and webhook dispatch should only see real active automations, while manual draft test runs still need a valid automation row to attach run history, telemetry, tarballs, sandbox state, and dispatcher execution config.
AutomationRun, and that run
must point at an automations.id.
Solution
Store partial setup state in automation_drafts.draft_body.
Validate the JSON against endpoint-specific partial Pydantic schemas.
Use the real creation schemas to decide whether the draft is dispatchable.
Create or update a linked Automation row for test dispatch.
Create a manual run tied to the materialized draft automation.
The materialized row is marked lifecycle_status = DRAFT and
enabled = false. That keeps it out of automatic scheduling
and event matching while making it clear that the row exists to support
draft testing, not because the user disabled a real automation.
automation_drafts, not in nullable executable columns on
automations.
/v1,
/v1/preset/prompt, or /v1/preset/plugin.
enabled remains the
backward-compatible active flag. lifecycle_status captures
the richer states: ACTIVE, INACTIVE, and
DRAFT.
Alternatives
| Approach | Upside | Tradeoffs | Decision |
|---|---|---|---|
Store drafts directly in automations |
One table; draft test runs naturally point at an automation row. | Requires many executable fields to become nullable or overloaded. Schedulers, dispatchers, serializers, and API responses must constantly handle incomplete automation rows. | Rejected |
Use only enabled = false |
No new lifecycle enum or state column. | Cannot distinguish a user-disabled production automation from a materialized draft test automation. That ambiguity leaks into UI, dispatch, git sync, cleanup, analytics, and future state transitions. | Rejected |
| Keep drafts only as untyped JSON | Very flexible and easy for clients to save anything. | Allows endpoint drift and arbitrary payloads. Errors surface late, and clients lose reliable field-level feedback for setup forms. | Rejected |
| Dispatch drafts directly from JSON | Avoids materializing a draft automation row. |
Conflicts with the current run model and dispatcher contract: runs
require an automation_id, and execution config is loaded
from Automation. Changing that path would be wider and
riskier.
|
Rejected |
| Separate drafts plus lifecycle materialization | Keeps incomplete form state separate, preserves dispatcher invariants, supports manual test runs, and keeps disabled vs draft semantics explicit. | Adds one lifecycle column and a materialization step when testing a draft. | Chosen |
Why this is the right compromise
This implementation isolates incomplete setup state where it belongs while using a normal automation row only at the point where the existing system requires one: run creation and dispatch. The lifecycle column is not trying to make drafts live in the automations table. It describes the status of the materialized executable row and prevents us from pretending every non-enabled row has the same meaning.
automation_drafts answers “what is the user editing?”automations.lifecycle_status answers “what kind of automation row is this?”automations.enabled answers “can this row be triggered automatically?”