Skip to content

Epic: scheduled workflows — cron schedules, nested runs, and the end of hardcoded dispatch #19

Description

@FreshlyBrewedCode

What to build

Replace the hardcoded GitHub-Projects dispatcher with config-defined schedules. A schedule is a workflow, its inputs, and a cron expression — nothing more. Preconditions move out of the daemon and into ordinary workflow bodies: a scheduled wrapper workflow checks whatever it needs to check (with an agent, if it wants one) and then dispatches child runs of the real workflow with the right inputs.

That makes nested runs the load-bearing piece. Without them a scheduled workflow would have to loop over every eligible item inside a single run — one workspace, one transcript, no parallelism, and one failure killing the batch. With them, each item gets its own run, its own history, and its own failure.

Today's dispatcher hardcodes one source (project board Status=Ready), one workflow, and joins run history on an issueNumber input field. All three are project policy that leaked into the runtime.

Decisions

Recorded here at a high level. Where a decision in this epic turned out to need a durable record, it lives in docs/adr/.

  • Cron strings in config, not an opaque Schedule combinator. A combinator cannot tell you when it next fires, cannot be validated at config load, and cannot be rendered. Effect's Cron gives parse-time validation and a computable next-fire time; Schedule.cron stays an implementation detail of the loop. Timezone is always explicit, never the daemon's local zone.
  • ctx.dispatch is fire-and-forget. A parent awaiting a child while holding one of the concurrency slots deadlocks the pool by construction. A child's outcome is observable in the log and the UI, never in the parent.
  • A dedupe key replaces the claim lock. Deleting the project-board claim removes the only thing preventing repeated dispatch of the same item. A generic key, rejected while a non-terminal run holds it, generalizes it and keeps wrappers idempotent — so "re-check on the next tick" becomes the natural retry.
  • Collisions throw; they do not silently no-op. A wrapper whose dispatches are all quietly dropped is indistinguishable from one that isn't running. Making failure states visible is the point of the UI.
  • Workspace is a tagged union (clone | scratch), not an optional. A nullable ctx.dir would poison exec, the agent adapter, and write-back with null branches. An agent needs a working directory, not a repo — so an agent-driven precondition check runs fine on scratch.
  • Dispatch requires the daemon. In-process execution is legacy; ctx.dispatch without a daemon throws.
  • No catch-up after a restart. Missed cron windows are skipped rather than replayed; runOnStart is available per schedule.
  • Precedence, matching the existing rule for agent options: run request > schedule > workflow > config default.

Subtasks

Out of scope for this epic

  • Failure backoff (Failure backoff for dispatch, keyed on dedupe key #20) — deliberately kept out and tracked as an optional standalone follow-up. The retired dispatcher backed off per failing issue; schedules and the sweep port ship without it, and a persistently failing item is simply retried each tick.
  • Configurable sandboxes. The run directory currently does three jobs at once — source material, host exec cwd, and the agent's view — identical only because the sandbox is the host. The two axes worth separating eventually are where a run executes and what is in it when it starts; this epic only makes the second configurable, and adds the execution seam without using it.
  • Delegating workspace materialization to the sandbox library. Factory owns cloning today; whether to hand that to the agent-sandbox layer's own workspace concept is a decision for whenever a non-host sandbox actually arrives.
  • Runtime enable/disable of schedules. Config-only for now; anything else needs schedule state in the database.
  • Cascading cancellation of child runs. The parent/child link is recorded; cancelling remains per-run.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions