---
title: "Recurring Jobs"
description: "Cron-scheduled prompts the agent runs on its own — daily digests, weekly reports, hourly polling."
---

# Recurring Jobs

A **recurring job** is a prompt that runs on a cron schedule. It's how the agent does things on its own: "every morning at 7 summarize my overnight emails," "every Monday post last week's signup numbers to Slack," "every hour sweep for stale drafts and delete them."

Recurring jobs fire on a clock. To react to _events_ (a booking created, an email received) — same `jobs/` file format plus conditions — see [Automations](/docs/automations).

Jobs live in [Agent Resources](/docs/agent-resources) at `jobs/<name>.md` — just a Markdown file with YAML frontmatter. No registration, no wiring. Drop the file in and the framework picks it up.

<Callout id="doc-block-rj1" tone="info">

**New to recurring jobs?** Jump to [Creating a job](#creating) — most people create one by asking the agent or with the Resources tab `+` menu, not by hand-editing the file format below.

</Callout>

## A job file {#job-file}

<AnnotatedCode
  id="doc-block-3pl82t"
  title="jobs/morning-digest.md"
  filename="jobs/morning-digest.md"
  language="markdown"
  code={
    '---\nschedule: "0 7 * * *"\nenabled: true\nrunAs: creator\n---\n\n# Morning digest\n\nSummarize the emails received overnight. Group by sender domain.\nPin the top 3 threads that look like they need a reply today to the\n"Needs reply" label. Draft replies for any that are obvious.'
  }
  annotations={[
    {
      lines: "2",
      label: "When",
      note: "Standard 5-field cron — `0 7 * * *` is every day at 07:00.",
    },
    {
      lines: "3",
      label: "Pause switch",
      note: "Flip to `false` to stop the job without deleting it.",
    },
    {
      lines: "4",
      label: "Identity",
      note: "`creator` runs with the owner's identity and `ANTHROPIC_API_KEY`; `shared` uses the org's key.",
    },
    {
      lines: "7-12",
      label: "The prompt",
      note: "The body is just a prompt — the agent runs it at each firing with all its normal tools and agent context.",
    },
  ]}
/>

That's it. The body is a prompt the agent runs at each scheduled firing. The agent has access to all the same tools and agent context it has in an interactive chat — actions, skills, memory, connected MCP servers, sub-agents.

## Frontmatter {#frontmatter}

| Field        | Type                          | Default      | Description                                                                                            |
| ------------ | ----------------------------- | ------------ | ------------------------------------------------------------------------------------------------------ |
| `schedule`   | cron expression               | _(required)_ | Standard 5-field cron. `"0 7 * * *"` = every day at 07:00; `"0 */4 * * *"` = every 4 hours.            |
| `enabled`    | boolean                       | `true`       | Flip to `false` to pause without deleting the job.                                                     |
| `runAs`      | `"creator"` \| `"shared"`     | `"creator"`  | `"creator"` runs with the job owner's identity and `ANTHROPIC_API_KEY`. `"shared"` uses the org's key. |
| `createdBy`  | email                         | _(auto)_     | Populated when the job is created through the Agent Resources UI or by the agent.                      |
| `orgId`      | string                        | _(auto)_     | Org scope; inherited from the creator's active org.                                                    |
| `lastRun`    | ISO timestamp                 | _(managed)_  | Written by the scheduler after each run.                                                               |
| `lastStatus` | `"success"` \| `"error"` \| … | _(managed)_  | Latest outcome.                                                                                        |
| `lastError`  | string                        | _(managed)_  | Error message if the last run failed.                                                                  |
| `nextRun`    | ISO timestamp                 | _(managed)_  | Computed from `schedule`; used by the scheduler to decide when to fire next.                           |
| `mcpTools`   | array of MCP tool names       | _(none)_     | Optional explicit allowlist such as `mcp__server__tool`; credentials stay in the connected MCP grant.  |

The `last*` and `nextRun` fields are written by the scheduler. You can read them to see the history, but don't edit them by hand — the next run will overwrite.

## Connecting MCP tools to a job {#connected-mcps}

Recurring jobs can use remote or local MCP tools that are already connected to
the app. Ask the agent to discover the exact names with `tool-search`, then
bind only the tools that job needs with `mcpTools`:

```md
---
schedule: "0 * * * *"
enabled: true
mcpTools:
  ["mcp__meeting-notes__list_recordings", "mcp__meeting-notes__get_transcript"]
---

Find new recordings since the last successful run. Extract only actionable
follow-ups, then call the app's bounded import action once with stable source
and external ids, a short citation, and the recording URL. Do not import the
full transcript.
```

The job stores tool names, not MCP URLs, access tokens, or OAuth refresh
tokens. At run time, the framework resolves the connected grant in the job's
creator or organization request context and exposes only the allowlisted
tools. Keep the list narrow, and use an idempotent app action for the final
import so overlapping runs do not create duplicates. See [MCP Clients](/docs/mcp-clients)
for connection and OAuth setup.

## Cron syntax {#cron}

Standard 5-field cron (minute, hour, day-of-month, month, day-of-week):

| Cron           | Meaning                  |
| -------------- | ------------------------ |
| `*/5 * * * *`  | Every 5 minutes          |
| `0 * * * *`    | Every hour on the hour   |
| `0 */4 * * *`  | Every 4 hours            |
| `0 7 * * *`    | Every day at 07:00       |
| `0 9 * * 1`    | Every Monday at 09:00    |
| `0 17 * * 1-5` | Weekdays at 17:00        |
| `0 0 1 * *`    | First day of every month |

The framework includes cron utilities (`isValidCron()` and `describeCron()`) for validating and rendering cron strings, used internally by the resource and scheduler layers.

## Creating a job {#creating}

### From the Resources tab

`+` → **Scheduled Task** in the Resources tab. Fill in the prompt and schedule. Saves as `jobs/<slug>.md` and starts running on the next matching tick.

### By asking the agent

> "Create a scheduled task that summarizes my unread emails every morning at 7."

The agent writes the file for you.

### By hand

Drop a Markdown file in `jobs/` via the framework's resource APIs:

```ts
import { resourcePut } from "@agent-native/core/resources";

await resourcePut(
  ownerEmail,
  "jobs/morning-digest.md",
  `---
schedule: "0 7 * * *"
enabled: true
---
Summarize overnight emails.`,
);
```

## How the scheduler runs {#how-scheduler-runs}

The scheduler is a framework plugin (the internal `processRecurringJobs()` routine) that runs in-process: a `setInterval` fires every 60 seconds (with a 10-second startup delay) inside the agent chat plugin, wherever the server is running.

<Diagram id="doc-block-14nqnkx" title="One scheduler tick" summary="Every 60s the scheduler finds due jobs, runs each as a fresh agent thread, and writes the outcome back to the job file.">

```html
<div class="sched">
  <div class="diagram-box accent">
    <code>setInterval</code> &bull; 60s &#8635;
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">1 &middot; scan</span
    ><small class="diagram-muted"
      >list every enabled <code>jobs/*.md</code> across all owners</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">2 &middot; due?</span
    ><small class="diagram-muted">compare <code>nextRun</code> to now</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card accent">
    <span class="diagram-pill accent">3 &middot; run</span
    ><small class="diagram-muted"
      >fresh agent thread, job body as the user message &mdash; actions, SQL,
      A2A, email</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card ok">
    <span class="diagram-pill ok">4 &middot; record</span
    ><small class="diagram-muted"
      >write <code>lastRun</code> / <code>lastStatus</code> /
      <code>lastError</code>, recompute <code>nextRun</code></small
    >
  </div>
</div>
```

```css
.sched {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 520px;
}
.sched .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
}
.sched .diagram-box {
  align-self: flex-start;
}
.sched .diagram-arrow {
  font-size: 18px;
  align-self: center;
}
```

</Diagram>

<Callout id="doc-block-j6yn07" tone="risk">

**Scale-to-zero caveat.** The scheduler is in-process, so on serverless hosts jobs only fire while an instance is warm. If reliable scheduling matters, keep an instance warm with keep-alive pings or use an always-on host (Fly, Render, a VPS).

</Callout>

<Callout id="doc-block-rj2" tone="warning">

**Disabled by default in local dev.** The scheduler's `setInterval` loop never starts when `NODE_ENV` is `development` or `test`, or when the app URL points at `localhost` / `127.0.0.1` / `*.localhost` — which covers most local dev setups. Set `AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS=1` to run the scheduler locally anyway. On a hosted deployment that runs its own dedicated sweep instead of this in-process scheduler, set `AGENT_NATIVE_DISABLE_RECURRING_JOBS=1` as the matching kill switch.

</Callout>

## Debugging a job {#debugging}

- Open `jobs/<name>.md` in Agent Resources — the frontmatter shows `lastRun`, `lastStatus`, `lastError`, `nextRun`.
- **Test it without waiting:** there's no force-fire tool. To exercise the same work on demand, either paste the job's prompt into the agent chat and let it run there, or temporarily set the schedule to the next minute so the scheduler picks it up on the next tick (then restore the real cron). This only works if the scheduler is actually running — see the local-dev caveat above; set `AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS=1` first if you're testing on localhost.
- **Pause it:** flip `enabled: false`. The file stays put, just stops running.

## Agent tool {#agent-tool}

A single `manage-jobs` tool is registered in every template. The `action` parameter selects the operation:

| Action   | Parameters                                                                             | Purpose                                                      |
| -------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `create` | `name`, `schedule`, `instructions` (required); `scope`, `runAs`, `model`, `mcpTools`   | Create a new recurring job                                   |
| `list`   | `scope` (`personal`, `shared`, or all)                                                 | List all jobs with status (schedule, enabled, last/next run) |
| `update` | `name` (required); `schedule`, `instructions`, `enabled`, `runAs`, `model`, `mcpTools` | Edit an existing job                                         |
| `delete` | `name` (required)                                                                      | Delete a job — always confirm with the user first            |

`model` is optional on both `create` and `update` — pass a model id to override the channel/app/engine default for that one routine.

**Personal vs shared scope.** Each job lives in either personal scope (runs as and is visible only to the creator) or shared/org scope (runs on behalf of the creator but is visible to org members). The `scope` and `runAs` parameters control this at create time. Org admins can update or delete any shared job; non-admin members can only manage their own.

## Different from the scheduling package {#vs-scheduling-package}

Don't confuse recurring jobs with `@agent-native/scheduling`:

- **Recurring jobs (this page)** — cron-scheduled _prompts_ the agent runs in the background. Framework-level. Lives in Agent Resources. Runs on any agent-native app.
- **`@agent-native/scheduling`** — a reusable domain package for building calendar/booking features (event types, availability windows, bookings). Powers the `calendar` template and custom scheduling surfaces.

Recurring jobs are "how do I make the agent act on its own?" The scheduling package is "how do I build a calendar app?" Different concerns.

## What's next

- [**Automations**](/docs/automations) — add event triggers and conditions to the same `jobs/` format
- [**Agent Resources**](/docs/agent-resources) — where jobs live alongside skills, memory, and custom agents
- [**Actions**](/docs/actions) — the tools a job calls
- [**MCP Clients**](/docs/mcp-clients) — connect remote or local MCP tools and OAuth grants
- [**Agent Teams**](/docs/agent-teams) — jobs often spawn sub-agents to do parallel work
- [**Toolkit piece: Toolkit Capability Modules**](/docs/toolkit-capability-packages) — how this differs from the installable Scheduling package
