Beacon — a durable AI content-publishing agent

A reference study. One realistic workflow that exercises the entire kyrie platform: customer.http + customer.wasm + customer.script, a parallel flow, human-in-the-loop approval, notifications, and a recurring self-rescheduling job. Every guarantee below is verified by 19 acceptance checks running against the live engine — including a real worker crash and a sandbox-egress probe.

The story

Beacon is an AI-assisted content studio. A writer — or an AI agent — submits a draft. Before anything goes live, Beacon must vet it for brand-safety, enrich it with an AI summary, decide whether to auto-publish or send it to a human editor, publish to a live CMS exactly once, notify the author, and then keep watching published posts to refresh the ones that go stale — on a schedule, forever.

That is a durable workflow: steps call flaky third-party APIs, publishing is irreversible, an editor’s approval might take hours, a worker can crash mid-flight, and the freshness loop must run for months without drifting or double-firing. Building that yourself means queues, retries, idempotency keys, a scheduler, a state machine, and a database. On kyrie it’s four task definitions, a flow, and ~150 lines of business logic.

What you’d build without kyrie

The hard partNormallyOn kyrie
Don’t re-charge / re-publish on retryhand-rolled idempotency + dedupe tablesctx.step(...) — runs once, ever
Survive a crash mid-workflowa saga / state machine + recovery codethe run resumes from its last step
Pause for a human for hoursa queue + a “waiting” table + a resume workerctx.waitFor(key)ResolveRun
Retry flaky HTTP safelybackoff + max-attempts + poison handlingtransient steps retry; memo keeps the rest
Recur every X without driftcron + locking + overlap guardsa self-rescheduling run + idempotency_key

Three primitives, each in its natural place

PrimitiveJob in BeaconWhy this primitive
customer.httpfetch the draft; call the AI service to summarizethe messy outside world — third-party / LLM APIs — and the natural place flakiness shows up
customer.wasmthe brand-safety guardrail: a compiled policy enginecompliance wants a reproducible, auditable, sandboxed verdict — same input, same answer
customer.scriptmerge signals into a verdict; durably publish; pause for the editororchestration plus the durable guarantees

Phase 1 — the automated vetting flow

 trigger: {draft_id}            ┌──────────────────────────────────────────┐
 ────────────────────────────► │  Flow: vet_draft                          │
                               │   fetch  (http GET /drafts/:id)            │
                               │     ├───────────────┬─────────────────────┐│
                               │     ▼               ▼                     ││  run in
                               │  guardrail        enrich                  ││  parallel
                               │  (wasm)           (http, flaky → retry)   ││
                               │     └───────┬───────┘                      ││
                               │             ▼                              ││
                               │          verdict (script)                  ││
                               │   auto · review · block                    ││
                               │   block ⇒ short-circuits the flow          ││
                               │   on_failure ⇒ alert                       ││
                               └────────────────────────────────────────────┘

A DAG that maps {{trigger.*}} and {{steps.<id>.output.*}} between steps, runs the guardrail and enrich steps in parallel, short-circuits when a draft is unsafe, and routes to an alert on failure.

Phase 2 — the durable publish

Human approval lives in a standalone durable script run — it parks in WAITING and resumes when an editor decides. Publishing is wrapped in ctx.step, so it happens exactly once even across a crash, retry, or resume.

export default async (input, ctx) => {
  if (input.decision === "review") {
    const review = await ctx.waitFor("editor");      // parks for hours, zero cost
    if (!review.approve) {
      await ctx.step("notify-changes", () => notify(input.draft_id));
      return { published: false };
    }
  }
  // idempotent: runs once even if the run resumes or retries
  const post = await ctx.step("publish", () => cms.publish(input.draft_id));
  return { published: true, post_id: post.id };
};

The edge — a recurring freshness monitor

kyrie has no cron primitive; you compose recurrence from a self-rescheduling run. The monitor checks for stale posts, conditionally fires a refresh event, then schedules its own successor by calling kyrie’s API — continue-as-new, with an idempotent re-arm so a retried tick never schedules two.

        ┌──────────────────────────────────────────────────────────┐
  tick →│  monitor (script, scheduled)                              │
        │   1. GET /cms/stale-check                                  │
        │   2. if stale → PublishEvent("draft.refresh_due") ──┐      │
        │   3. reschedule self: CreateRun(in_seconds, tick+1) │      │
        └──────────────────────────────────────────────────────┼─────┘
                 ▲                                              │ trigger
                 └──── next tick ◄───────── re-vet flow ◄───────┘

A stale post loops back through Phase 1; a fresh post is left alone.

Conditional branching — three flavors

MechanismIn BeaconReads as
Flow short-circuitverdict blocks an unsafe draft”if unsafe → stop the pipeline”
Event triggerrefresh event fired only for stale posts”if stale → start the re-vet journey”
In-script if + ctx.waitForreview → human gate; auto → publish”if borderline → ask a human”

What it proves

  • Never double-publishctx.step memoization; exactly-once under resubmit.
  • Survives crashes — the suite kills the worker process mid-run; the run resumes from its durable memo and never re-publishes.
  • Hours-long human approvalctx.waitFor parks at zero cost; ResolveRun resumes.
  • Safe flaky integrations — transient retries without losing prior work.
  • Recurring jobs, no cron infra — self-rescheduling runs, dedup-safe.
  • Runs untrusted code safely — a script proves it can reach only its declared allow_net hosts; everything else is refused.

Every line above is checked by the Beacon acceptance suite — 19 of 19 passing against the live engine: the vetting flow, the human-gated publish, the recurring monitor, a real worker crash, and a sandbox-egress probe.


Ready to build your own? Open the kyrie app.