chloejsnpm i chloejs

Docs

A job is a workflow

Steps, state, replay, and the one rule that makes carrying on after a pause safe.


A job is an input, a state and steps. It is an async function, so branching and looping are if and for: a job can stop early (if every site is up, end), loop (each of five sites) and wait (ask before restarting). Carrying on after a pause means running the function again from the top, with every finished step handing back what it returned last time.

One rule makes that safe: work happens inside a step, and code outside a step only decides. A step is written down, so it never runs twice. A line outside a step runs again every time the job resumes, so it must not send, write or spend.

That is the price of if and for instead of a builder API, and it is the trap worth knowing before you write your second job.

What a job is handed

  • step(name, fn) does something, once, and writes down what it returned.
  • model(name, options) asks a model one question and validates the answer against a zod schema.
  • agent(name, options) hands a goal and some tools to a model and lets it choose the order, inside the tools it was given and the cap you set.
  • ask(name, options) stops and waits for a person. See asking a person.

The first three are the autonomy hierarchy: reach down it, not up it.

  • state is the shared store, with a schema of its own, and it survives a pause. A step's result is for the step after it; state is what the whole job is accumulating. Two channels, not one bag, which is why a parked job can show somebody what it already found before it asks them anything.

A job says what it did

summary turns what run returned into one line, and that line is what the overview shows beside the run. The job knows what its result means, so the page never guesses. A job without one shows no line, and a prompt's line is the start of its reply.

A job that was edited under a parked run

The replay checks each step's name as well as its place, because handing the wrong recorded answer to the wrong step would look like it worked. If the job changed, the run stops and says so. Start it again.

Two runs never overlap

Whether it is code or a prompt, a second run of the same job is skipped rather than queued, and a job already waiting on somebody is not started again. A job that takes longer than its own interval quietly runs less often than its cron line says.

When a job runs

cron is a cron line, and chloejs/timer writes one in words:

every(15).minutes               // */15 * * * *
every(4).hours                  // 0 */4 * * *
every.hour.at(30)               // 30 * * * *
every.day.at("07:00")           // 0 7 * * *
every.day.at("10:45", "22:45")  // 45 10,22 * * *
every.weekday.at("9:30")        // 30 9 * * 1-5
every.monday.at("9:00")         // 0 9 * * 1
every.month.on(1).at("09:00")   // 0 9 1 * *

What a cron line cannot say evenly is refused as the file loads, with the reason and the file's name: every(7).minutes would run at :56 and again at :00, every.month.on(31) would skip short months, and two times of day with different minutes need two jobs. A plain cron string still works.

timezone is a real timezone name and daylight saving is handled through Intl, so write every.day.at("07:00") with timezone: "America/New_York" and stop doing UTC arithmetic in a comment.

Without a cron a job runs only when somebody starts it: the Run button, npm run agent <agent> <job>, or POST /api/jobs/<agent>/<id>/run.