Skip to content

Repository files navigation

Runway

Runway is a TypeScript-first authoring layer over a language-neutral workflow and repository-runner foundation on Cloudflare. Author workflows with workflow({ id, secrets?, tools?, trigger? }).run(async (step, event) => { ... }) and export them from .runway/workflows/**/*.ts.

Repository execution and managed CI/CD come first. Cloudflare Sandbox, cache transport, credentials, terminal coordination, metering, and resource reconciliation stay internal. Runway does not own a package-manager preset, dependency graph, build scheduler, or public Sandbox API. Agents are deferred.

This project is in development, not production. The foundation is deployed for its own repository CI; comparative release claims and publication remain gated. See Development Status.

For domain vocabulary, direction, next steps, and open issues, see VISION.md.

A Workflow

Start with offline scaffolding:

npm add --save-dev runway
npx runway init

This creates only .runway/workflows/example.ts and preserves it on repeated initialization. Run it directly in the current checkout:

npx runway run example
// .runway/workflows/hello.ts
import { cron, mise, workflow } from "runway";

export default workflow({
  id: "hello",
  tools: mise(),
  trigger: () => cron("0 9 * * *"),
}).run(async (step, event) => {
  await step.cache("runtime", {
    key: { prefix: "runtime-linux-", files: ["runtime.lock"] },
    restoreKeys: ["runtime-linux-"],
    paths: ["/cache/runtime", ".runtime"],
  });
  const greeting = await step.do("greet", () => "hello");
  await step.exec("check", "./scripts/check");
  await step.sleep("wait", 5000);
  await step.do("finish", () => `${greeting} world at ${event.scheduledTime}`);
});

Default exports, named exports, and barrel re-exports are supported. The run callback receives the typed trigger event and one flat author surface:

interface Step<Secrets extends string = string> {
  readonly runId: string;
  readonly secrets: { readonly [Name in Secrets]: string };
  do<T>(id: string, work: () => T | Promise<T>): Promise<T>;
  exec(id: string, command: string | ExecOptions): Promise<ExecResult>;
  cache(id: string, declaration: CacheDeclaration): Promise<CacheResult>;
  sleep(id: string, durationMs: number): Promise<void>;
}

Use step.do() for replayable work, step.exec() for managed commands, step.cache() for generic filesystem trees, and step.sleep() for durable waits. Give every operation a stable 1–128 UTF-8 byte id that does not begin with runway:. Durable operation bodies must be idempotent and return JSON-serializable values.

await step.exec("check", {
  command: "./scripts/check",
  cwd: "packages/app",
  env: { MODE: "ci" },
  timeoutMs: 20 * 60_000,
});

Managed commands default to /workspace, CI=true, and a 15-minute timeout. They do not receive workflow secrets automatically. A non-zero exit throws a secret-safe ExecError; the result and diagnostics contain bounded, redacted output tails.

Local Runs

runway run <id> invokes a workflow directly in the current checkout. Its trigger does not restrict explicit invocation, so a GitHub-triggered workflow such as Runway's own check can run without an event when its callback does not use one:

runway run check
runway run event-dependent-workflow --event event.json
cat event.json | runway run event-dependent-workflow --event -

Event files contain the normalized JSON value expected by the callback, not a raw provider webhook. When omitted, the callback receives undefined. Local execution is intentionally small: commands use the host shell and environment, workflows with declared secrets are rejected, tool providers are not provisioned, cache operations are skipped, and do and sleep are non-durable. It does not emulate Cloudflare retries, Sandbox placement, Source reconstruction, or cache publication.

Exact Source And Continuity

Every repository run binds a credential-free HTTPS remote to an exact 40-character Git revision. The Sandbox prepares /workspace lazily and verifies HEAD. Private GitHub checkout uses a repository-scoped installation token only in the checkout process environment; the token is removed before authored commands and never becomes Source evidence.

One Sandbox belongs to one run and exact Source. A retry may reconnect only when placement, process, and command digest prove continuity. If a Sandbox is replaced before any command may have started, Runway can reconstruct the same Source. Once a command has started or may have started, unproven placement loss fails the run instead of replaying possible filesystem or external side effects. Caches do not change this rule and are not workspace checkpoints.

Tools And Generic Cache

Mise is the common-case provider. It discovers repository configuration by default, or accepts a small inline tool map. Its pinned mise binary, installed tools, shims, and required runtime files are cached automatically:

tools: mise();
tools: mise({ node: "26.5.0", pnpm: "11.5.0" });

Providers are ordered, so a workflow can mix mise with a native release or a provider built with defineToolProvider():

workflow({
  id: "check",
  tools: [
    mise(),
    release({
      name: "aube",
      version: "1.2.3",
      url: "https://example.com/aube-1.2.3-linux-amd64.tar.gz",
      sha256: "0000000000000000000000000000000000000000000000000000000000000000",
      executable: "aube",
    }),
  ],
  trigger: () => cron("0 9 * * *"),
}).run(async (step) => {
  await step.exec("check", "aube check");
});

step.cache() declares caller-owned trees before command execution:

const result = await step.cache("compiler-state", {
  key: { prefix: "compiler-linux-", files: ["compiler.lock", "project.json"] },
  restoreKeys: ["compiler-linux-"],
  paths: ["/cache/compiler-state", ".compiler"],
});

The foundation knows paths, content keys, platform identity, trust, integrity, and budgets. It knows nothing about pnpm, Python, Rust, or any other ecosystem. Relative targets resolve below /workspace; absolute targets are restricted to safe roots below /workspace or /cache. Cache restore happens before any command. A cache may be published only after the durable terminal winner is success; failed, cancelled, superseded, unsafe, corrupt, or over-budget producers do not advance a ref.

Payloads are private, content-addressed SquashFS objects transferred directly between the Sandbox and R2 through short-lived exact-object capabilities. Restore verifies identity and integrity in a sibling staging tree before an atomic rename. Cache schema 2 and runner ABI runway-sandbox-v2 append a bounded canonical private hardlink trailer to preserve regular-file identity with the pinned image's high-level squashfuse. This is private encoding, not a public snapshot or content-store API.

The foundation includes safe restore and success-only publication behavior. Live private-R2 runs proved miss, publication, warm restore, and corrupt-input handling. Runway keeps cache use evidence-driven: its own repository removed whole-tree caches after they lost on both latency and cost.

Triggers And Secrets

Triggers are optional, and omitting one creates no automatic ingress or public start endpoint.

  • An omitted trigger gives an undefined event.
  • cron("0 9 * * *") gives event: { cron, scheduledTime }.
  • github({ checkName, events }) selects pushes and pull requests, binds each accepted delivery to its exact repository and SHA, and keeps the GitHub Checks lifecycle internal.
  • webhook({ path, secret, signatureHeader, schema? }) verifies the raw POST body with HMAC-SHA256.
  • webhook({ path, secret, signatureHeader, schema }) validates with Standard Schema and types the event as its output.
  • webhook<T>({ path, secret, signatureHeader }) is assertion-only typing; the same required options without a generic give unknown.
  • .filter(typeGuard) narrows and gates a trigger event after validation.

Declare every workflow secret, including webhook signing secrets. In trigger(ctx), ctx.secrets.X is a branded name reference; in the run callback, step.secrets.X is the captured runtime string. Internal publication fails before upload when a declared secret is missing.

GitHub triggers use internal RUNWAY_GITHUB_APP_ID, RUNWAY_GITHUB_PRIVATE_KEY, and RUNWAY_GITHUB_WEBHOOK_SECRET bindings. They never enter authored workflow secrets or repository commands.

Internal Publication And Stack Ownership

The internal publication path discovers .runway/workflows/**/*.ts, builds immutable content-addressed workflow artifacts, and creates one repo-scoped Cloudflare Stack. A Stack manifest and immutable receipt bind the exact Worker version/deployment, Dynamic Workflow, container image and rollout, Durable Object namespaces, schedules, routes, bindings, secret names, workers.dev state, and exact owned bucket objects. Sync and removal re-inventory the provider and fail closed on drift. Unknown state and shared objects are preserved.

The private runway-data and runway-state buckets are shared by the account; Stack removal must preserve them and any object not claimed by exact ownership evidence. A deployment name is derived from the Git repository: runway for this repository and runway-<repository> otherwise. Its Worker, Dynamic Workflow, and container use that exact name. Cloudflare derives its Durable Object namespace names from the Worker and class names. The digest-pinned linux/amd64 Sandbox uses standard-4. Capacity remains an internal foundation choice, not a public workflow option.

Runway does not own or store Cloudflare credentials. Cloud commands use an installed Wrangler, Cloudflare's official CLI. They use an existing Wrangler login or launch the ordinary wrangler login flow lazily in an interactive terminal. If Wrangler is not installed, provide CLOUDFLARE_API_TOKEN and, when needed, CLOUDFLARE_ACCOUNT_ID. Set authored secrets with runway secrets set NAME .... Deployment identity cannot be configured through package.json or environment variables.

GitHub App Setup

Create a GitHub App named Runway with Contents read, Pull requests read, Checks write, and Push and Pull request subscriptions. Install it only on repositories Runway should serve. Set its webhook URL to the deployed /.runway/github endpoint and use the same webhook secret as RUNWAY_GITHUB_WEBHOOK_SECRET.

export RUNWAY_GITHUB_APP_ID=12345
export RUNWAY_GITHUB_PRIVATE_KEY="$(cat /path/to/github-app.pem)"
export RUNWAY_GITHUB_WEBHOOK_SECRET="$(openssl rand -hex 32)"

Development Status

The root Check and Test workflows are ordinary Runway consumers. They discover the repository mise configuration for Node and Aube and run ordinary mise tasks that automatically reconcile dependencies, without caching application dependencies. Mise installs tools fresh because transporting its whole data tree costs more than setup; providers use the generic cache only when measured restore cost wins.

At PR head df10a82 on 2026-07-17, 15 sequential development samples on the deployed runway integration produced Check P50/P95 of 39s/46s, Test P50/P95 of 87s/102s, and delivery-to-terminal P50/P95 of 96s/105s, with no cache operations. The earlier whole-tree cache experiment took 2m23s/3m25s cold and 3m28s/4m14s warm, plus about $0.013 of cache work per warm run, so it was removed rather than abstracted into the foundation.

The exact runway Stack runs the digest-pinned linux/amd64 Sandbox on standard-4.

Runway's Check/Test workflows are the only repository CI. Do not add a duplicate GitHub Actions fallback.

Scope

Implemented locally: workflow authoring, triggers, secrets, exact Source, durable operations, run-bound Sandbox execution, one Terminal authority, generic cache identity/restore/publication, Meter lifecycle timings, immutable workflow artifacts, GitHub delivery and Checks coordination, and exact Stack ownership/reconciliation.

Still intentionally deferred: comparative release benchmarking, ecosystem-specific application cache adapters, and final publication. Cloudflare Artifacts is a future evidence-gated Source implementation only; it is not the cache store. BuildKit, run artifacts, deployment workflows, AI, and agents are later consumers.

Testing

Run the individual checks and test suite with mise:

mise run format-check
mise run lint
mise run typecheck
mise run fallow
mise run test

Run the opt-in end-to-end suite separately:

mise run test:e2e

It runs the exact pinned-image cache contract followed by live Cloudflare cache, artifact, repository, and GitHub recovery smokes. It is not part of routine CI and requires privileged Docker with linux/amd64 support, Wrangler authentication, and R2 S3 credentials in RUNWAY_CACHE_SMOKE_R2_ACCESS_KEY_ID and RUNWAY_CACHE_SMOKE_R2_SECRET_ACCESS_KEY. Set RUNWAY_LIVE_GITHUB_RECOVERY=1 together with RUNWAY_GITHUB_APP_ID, RUNWAY_GITHUB_PRIVATE_KEY, RUNWAY_LIVE_GITHUB_REPOSITORY, and RUNWAY_LIVE_GITHUB_SHA to include private GitHub recovery; otherwise that final smoke is skipped.

About

Workflow infrastructure for repository automation and AI agents on Cloudflare.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages