# Run an AI agent on a schedule

> Give a job a cron line and it runs on its own, in your timezone, with every run written down and no two runs overlapping.

A scheduled AI agent is a job with a `cron` line. `npx chloe` keeps every cron
line of every agent, so there is no separate scheduler, queue or cron table to
set up.

## A job on a timer

`every` from `@chloejs/core/timer` writes the cron line in words, and
`timezone` is a real timezone name, so daylight saving is handled for you. This
one checks the orders every two hours and never asks a model, so it costs
nothing to run:

```ts
export default defineJob({
  id: "stuck-orders",
  cron: every(2).hours,
  timezone: "America/New_York",
  description: "Finds orders that are paid for and late, and tells the warehouse.",
  run: async (work) => {
    const all = await work.step("read the orders", () => orders());

    const late = all.filter((one) => stuck(one));
    if (late.length === 0) return { checked: all.length, late: [], worth: 0 };

    const worth = late.reduce((total, one) => total + one.total, 0);
    await work.step("tell the warehouse", () =>
      tellTheWarehouse(
        `${late.length} paid order${late.length === 1 ? "" : "s"} past the promised date`,
        late.map((one) => `${one.id}: promised ${one.promised.slice(0, 10)}, ${money(one.total)}, ${one.lines.length} line(s)`),
      ),
    );

    return { checked: all.length, late: late.map((one) => one.id), worth };
  },
  response: (r) =>
    r.late.length === 0
      ? `${r.checked} orders, none late`
      : `${r.late.length} late, ${money(r.worth)} sitting: ${r.late.join(", ")}`,
});
```

The same works for a job with a model in it. This one sorts the inbox every 15
minutes, and one model call sorts everything that came in, so the price is per
run and not per message:

```ts
export default defineJob({
  id: "sort-messages",
  cron: every(15).minutes,
  timezone: "America/New_York",
  description: "Reads what customers wrote in and puts each one on the right desk.",
  model: "anthropic/claude-haiku-4.5",
  run: async (work) => {
    const waiting = await work.step("read what came in", () => unread());
    if (waiting.length === 0) return { sorted: 0, urgent: [], desks: {} as Record<string, number> };

    const read = await work.model("work out what each one is about", {
      instructions:
        "You sort a shop's inbox. Judge only what the customer wants, not what should be done about it. " +
        "Every message gets exactly one line back, with the id copied exactly.",
      output: Sorted,
      prompt: waiting.map((one) => `${one.id} (${one.at}): ${one.text}`).join("\n\n"),
    });

    // The rules, in code: where each one goes, and which are worth interrupting
    // somebody for. The model never decides either.
    const desks: Record<string, number> = {};
    for (const desk of new Set(read.sorted.map((one) => DESK[one.about]))) {
      const ids = read.sorted.filter((one) => DESK[one.about] === desk).map((one) => one.id);
      desks[desk] = await work.step(`hand ${ids.length} to ${desk}`, () => handTo(desk, ids));
    }

    const urgent = read.sorted.filter((one) => one.urgency === "today" || one.wantsMoneyBack);
    return { sorted: read.sorted.length, urgent: urgent.map((one) => `${one.id}: ${one.inAWord}`), desks };
  },
  response: (r) =>
    r.sorted === 0
      ? "nothing came in"
      : `${r.sorted} sorted into ${Object.keys(r.desks).length} desk(s)` +
        (r.urgent.length ? `, ${r.urgent.length} for today: ${r.urgent.join("; ")}` : ""),
});
```

## A prompt on a timer

A job can also be only a prompt, with the words in a markdown file beside it.
This one runs every Sunday evening and decides for itself whether there is
anything to say:

```ts
// The one job here that is a prompt from end to end.
//
// The words are in how-it-went.md beside this file. It is a prompt rather than
// code because what to make of a week of runs is not a rule anybody could write
// down, and because it decides for itself whether there is anything to say.
import { defineJob } from "@chloejs/core";
import { every } from "@chloejs/core/timer";

export default defineJob({
  id: "how-it-went",
  cron: every.sunday.at("18:00"),
  timezone: "America/New_York",
  description: "Reads its own notes and says whether anything about the shop has been quietly getting worse.",
  markdown: "jobs/how-it-went.md",
});
```

## What a schedule promises

- **No overlap.** A second run of the same job is skipped rather than queued,
  so a job that takes longer than its interval runs less often, never twice at
  once.
- **No silent mistakes.** A cron line that cannot be said evenly, like every 7
  minutes, is refused when the file loads, with the reason.
- **No lost waits.** A job waiting on a person survives a restart and carries
  on from where it stopped.
- **A record.** Every run, scheduled or not, writes down each step and what it
  cost.

Every form `every` takes is in [A job is a workflow](https://chloejs.org/docs/jobs#when-a-job-runs).
A job without a cron line runs only when somebody starts it: from the page, a
chat, the API or `npx chloe agent <agent> <job>`.

## Keep it running

`npx chloe` has to be running for anything to fire. `npx chloe install` makes it
a service and starts it, a systemd user unit on Linux and a launchd agent on a
Mac, so it keeps going after you close the terminal and after a reboot.

Next: [build an AI agent in TypeScript](https://chloejs.org/docs/build-an-ai-agent), or see
[the examples](https://chloejs.org/examples), each on its own schedule.
