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 part | Normally | On kyrie |
|---|---|---|
| Don’t re-charge / re-publish on retry | hand-rolled idempotency + dedupe tables | ctx.step(...) — runs once, ever |
| Survive a crash mid-workflow | a saga / state machine + recovery code | the run resumes from its last step |
| Pause for a human for hours | a queue + a “waiting” table + a resume worker | ctx.waitFor(key) → ResolveRun |
| Retry flaky HTTP safely | backoff + max-attempts + poison handling | transient steps retry; memo keeps the rest |
| Recur every X without drift | cron + locking + overlap guards | a self-rescheduling run + idempotency_key |
Three primitives, each in its natural place
| Primitive | Job in Beacon | Why this primitive |
|---|---|---|
customer.http | fetch the draft; call the AI service to summarize | the messy outside world — third-party / LLM APIs — and the natural place flakiness shows up |
customer.wasm | the brand-safety guardrail: a compiled policy engine | compliance wants a reproducible, auditable, sandboxed verdict — same input, same answer |
customer.script | merge signals into a verdict; durably publish; pause for the editor | orchestration 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
| Mechanism | In Beacon | Reads as |
|---|---|---|
| Flow short-circuit | verdict blocks an unsafe draft | ”if unsafe → stop the pipeline” |
| Event trigger | refresh event fired only for stale posts | ”if stale → start the re-vet journey” |
In-script if + ctx.waitFor | review → human gate; auto → publish | ”if borderline → ask a human” |
What it proves
- Never double-publish —
ctx.stepmemoization; 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 approval —
ctx.waitForparks at zero cost;ResolveRunresumes. - 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_nethosts; 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.