Most of a job is code
A model is one step, and only where a step needs judgement. Call it 95 and 5. The point is that you can always say which 5, because the file says so and the run record prices it.
export default defineJob({
id: "site-check",
cron: every.hour.at(0),
description: "Asks every site whether it answered, and writes down what it said.",
run: async (work) => {
const answers = await work.step("ask every site", () =>
// All at once, so one slow site does not hold up the rest.
Promise.all(SITES.map((site) => ask(site))),
);
const at = new Date().toISOString();
await work.step("write down what they said", () =>
note(work.agent, "sites", Sites).write({ at, answers }).then(() => at),
);
return { checked: answers.length, down: answers.filter(down).map((one) => one.site) };
},
summary: (r) => (r.down.length === 0 ? `${r.checked} sites, all answering` : `down: ${r.down.join(", ")}`),
});The whole file is on the ladder page, and it is a real one: the site quotes it out of the repo rather than keeping a copy.
That job never asks a model, and most jobs should not. When one has to, it is a step of its own with a shape for the answer:
const said = await work.model("write the morning line", {
model: "anthropic/claude-haiku-4.5",
output: z.object({ line: z.string().min(10).max(300) }),
prompt: `These are worth mentioning:\n${worrying.join("\n")}`,
});You can point at the line that asks a model.
A step that asks is named in the job, it answers in a shape, and the run record prices it. A job that quietly grew a second model call shows up as a second line and a bigger number.
A job is an async function, not a graph.
Branching is if and looping is for. Waiting for a person is one call, and the process may restart while it waits.
A job picks its own model in one line.
The cheapest model that can do the step is the right model for the step, and choosing it per job is how that stays true.
It is two packages
npm install chloejs chloejs-uichloejs is the runtime and its only dependency is zod. chloejs-ui is the page: the agents, their runs, a conversation and their files. A machine that only runs jobs does not need it.