Build your own software factory.

Infrastructure and APIs for your own agent harness.
Your agents. Your workflows. Your rules.

Assemble the factory yourself.It's all TypeScript, so nothing is fixed.

Devin, Cursor Cloud and Codex Cloud ship the agent as a finished product. Helmr ships it as infrastructure: every part is a TypeScript value you pick, swap, or write yourself. Change a part below and the file rewires itself.

Any job.

A one-shot task, a durable actor session, or a cron schedule.

Any agent.

Claude Code, Codex, an open harness on a cheap model, or the loop you wrote yourself.

Any interface.

Wherever your team already works. Helmr is the runtime behind it, not the chat window in front.

review-pr.ts@anthropic-ai/claude-agent-sdk
 1import { image, sandbox, source, task, tokens } from "@helmr/sdk" 2import { query } from "@anthropic-ai/claude-agent-sdk" 3import { z } from "zod" 4  5const base = image("repo-agent") 6  .from("node:24-bookworm-slim") 7  .workdir("/workspace") 8  .run(["sh", "-ceu", "apt-get update && apt-get install -y git ripgrep"]) 9  .copy(source.file("package.json"), "/workspace/package.json")10  .run(["bun", "install"])11 12export const repoSandbox = sandbox({ id: "repo-agent" })13  .image(base)14  .resources({ cpu: 2, memory: "4GiB" })15 16export const reviewPr = task({17  id: "review-pr",18  payload: z.object({ prNumber: z.number().int().positive() }),19  run: async (event, ctx) => {20    const brief = `Review PR #${event.prNumber} and propose a patch.`21 22    let output = ""23    for await (const message of query({24      prompt: brief,25      options: {26        cwd: "/workspace",27        permissionMode: "bypassPermissions", // the microVM is the sandbox28        allowDangerouslySkipPermissions: true29      }30    })) {31      if (message.type === "result" && message.subtype === "success") output = message.result32    }33 34    const approval = await tokens.create({ timeout: "30m" })35    await sendSlackApproval({36      channel: event.channel,37      callbackUrl: approval.callbackUrl38    })39    // The microVM freezes here — filesystem, memory, process.40    const decision = await approval.wait({41      schema: z.object({ approved: z.boolean() }),42      timeout: "30m",43      metadata: { subject: "Post this review to GitHub?" }44    }).unwrap()45    if (decision.approved) await postReview(event.prNumber, output)46  }47})

Workflows as functions. Typed in, typed out, durable in between.

Write the Task

task()typescript
export const reviewPr = task({
  id: "review-pr",
  payload: z.object({ prNumber: z.number() }),
  run: async ({ prNumber }) => {
    return draftReview(prNumber)
  }
})

One exported const with a typed payload and a run function. That is the whole unit Helmr deploys.

Start it from anywhere

client.tasks.start()typescript
const run = await client.tasks.start<typeof reviewPr>(
  "review-pr",
  {
    payload: { prNumber: 482 },
    workspace
  }
)

Your product, a webhook, or CI starts a deployed Task with a typed payload against an existing Workspace.

Put it on a cron

schedules.task()typescript
export const nightly = schedules.task({
  id: "nightly-audit",
  cron: {
    pattern: "0 9 * * 1-5", timezone: "Asia/Tokyo"
  },
  workspace: { sandbox: repoSandbox },
  run: async (_, ctx) => runAudit(ctx)
})

The schedule lives beside the code and ships with it. Every fire gets a fresh Workspace.

Wait for a human

tokens.create()typescript
const approval = await tokens.create({ timeout: "30m" })

const decision = await approval.wait({
  schema: z.object({ approved: z.boolean() }),
  timeout: "30m",
  tags: ["approval", "github-review"]
}).unwrap()

The microVM freezes whole while it waits — filesystem, memory, process. A scoped callback completes one value.

Steerable agent sessions. Input and output, kept across Runs.

Define the Actor

actor()typescript
export const reviewer = actor({
  id: "reviewer",
  async run(session) {
    for (;;) {
      const turn = await session.receive()
      if (turn === null) return
      await turn.onMessage(async ({ data }) => {
        await applyCorrection(data)
      })
      await turn.output.pipe(review(turn.input, {
        signal: turn.signal
      }))
      await runTests()
      await turn.complete()
    }
  }
})

Send input and read output

client.sessions.ref()typescript
// From your product or webhook:
const session = client.sessions.ref(sessionId)
const turn = await session.enqueue({ issue: "APP-42" })

// When this exact Turn is ready for messages:
await turn.send({
  type: "update_constraints",
  instruction: "Please also update the tests."
}, { idempotencyKey: "slack:thread-1:message-7" })

const page = await session.events.list({ after: 0 })
for (const event of page.records) {
  render(event.sequence, event.kind, event.data)
}

Send a correction from Slack, your product, or the CLI. The Session history survives waits and continuation Runs.

Persistent VM filesystems. The same /workspace, Run after Run.

Define the environment

image() + sandbox()typescript
const base = image("repo-agent")
  .from("node:24-bookworm-slim")
  .workdir("/workspace")
  .run(["npm", "install", "-g", "@openai/codex"])

export const repoSandbox = sandbox({
  id: "repo-agent"
})
  .image(base)
  .resources({
    cpu: 2,
    memory: "4GiB"
  })

Choose the base image, install CLIs, and set the working directory and resources behind the Workspace.

Create the Workspace

client.sandboxes.createWorkspace()typescript
const workspace = await client.sandboxes.createWorkspace(
  "repo-agent",
  {
    key: `github-pr:${pr.id}`,
    idempotencyKey: `workspace:${pr.id}`
  }
)

const result = await workspace.exec({
  command: ["bash", "-lc", "bun test"],
  cwd: "/workspace",
  idempotencyKey: `verify:${pr.id}`
})

Create it once, run bounded commands, and attach later Task and Actor Runs to the same files.

The agent never sees the key. Secrets are swapped in at the network boundary — outside the VM.

inside the microVMGH_TOKEN
GET /repos/acme/app/pulls/482
Host: api.github.com
Authorization: Bearer hlmr_p_3f9c…e21a
api.github.comallowed origin
GET /repos/acme/app/pulls/482
Host: api.github.com
Authorization: Bearer ghp_••••••••••••••••
anywhere elsenot allowed
POST /collect
Host: telemetry.example
Authorization: Bearer hlmr_p_3f9c…e21a

Bind a secret as protected and the VM only ever holds a placeholder. Helmr replaces it for the origins you allow, checks the run is still authorized, and fails the request if the secret was revoked.How protected secrets work →

Run on our cloud, or in your AWS.

Early, open, Apache 2.0. Read the source before you trust it with your repos.

apache-2.0go control planetypescript sdkfirecracker microVMsopentofu modules

Helmr Cloud coming soon

A managed fleet. Projects, environments, immutable deployments — no infrastructure to run.

Read the docs

Your AWS account

The identical control plane and workers in your VPC, Apache 2.0. Credentials never leave your boundary.

Self-hosting guide