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.
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
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
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
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
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
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
// 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
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
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.
GET /repos/acme/app/pulls/482
Host: api.github.com
Authorization: Bearer hlmr_p_3f9c…e21aGET /repos/acme/app/pulls/482
Host: api.github.com
Authorization: Bearer ghp_••••••••••••••••POST /collect
Host: telemetry.example
Authorization: Bearer hlmr_p_3f9c…e21aBind 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.
Helmr Cloud coming soon
A managed fleet. Projects, environments, immutable deployments — no infrastructure to run.
Read the docsYour AWS account
The identical control plane and workers in your VPC, Apache 2.0. Credentials never leave your boundary.
Self-hosting guide