# Jobs

Jobs are automated agent runs — started on demand, on a recurring schedule, or when an event arrives.

---

## What Are Jobs?

A job is one thing: a definition (which agent, and what to ask it) plus a **trigger** that decides when it runs. There is no second kind of object to learn — a job you start by hand and a job that fires on a schedule are the same record with a different trigger.

The Jobs app opens as a searchable table of every job you can see. Each row carries an icon for its trigger kind; hover the icon for a tooltip naming it.

| Trigger | Icon | Tooltip | How it works |
|---------|------|---------|--------------|
| **Manual** | Lightning bolt | *Manual* | Runs the agent once, when you start it. |
| **Cron** | Calendar clock | *Cron* | Runs the agent on a recurring schedule defined by a cron expression. |
| **Push** | Eye | *Push* | Listens for events. When one arrives, the job evaluates its filter and runs the agent if the event passes. |

Open a job and the same information appears as a badge on its detail page reading **Manual**, **Cron**, or **Push**.

Every trigger creates a **run** record you can inspect, so you always have visibility into what ran, when, and whether it succeeded.

![The Jobs app — the search box and Agent, App, Channel and Status filters above a job list, each row showing its trigger icon, agent, status, and last run](images/06-insulin-jobs.png)

### Finding a job

The list header holds a **Search…** box plus four filters:

| Filter | What it narrows to |
|--------|--------------------|
| **Agent** | Jobs that run under one agent. |
| **App** | Jobs created from one Insulin app. Jobs created before app provenance was recorded, or created by an agent with no app context, group under **Unassigned**. |
| **Channel** | Jobs whose agent belongs to one channel. |
| **Status** | Jobs whose latest run is in one state. |

Each filter opens with an "any" option (for example **Status: any**) that clears it. Filters apply server-side, so they hold as you scroll past the first page of results. Columns — **Agent**, **App**, **Channel**, **Status**, and **Last run** — can be dragged wider or narrower; **Name** absorbs the change.

A filter for something the window already pins is not offered. Open the list from an agent and there is no Agent filter; open it from a channel and there is no Channel filter — the control would only let you contradict the context you opened.

### The Jobs tab in a chat or a channel

Jobs also appear inside Chat. A solo agent chat and a [channel](/insulin/channels/) both show **Messages**, **Files**, and **Jobs** tabs.

A channel does not own jobs of its own. Its Jobs tab lists the jobs belonging to the agents that are members of that channel, resolved for you server-side — and only the ones you are allowed to see. This embedded view is a narrow panel, so it drops the search box and the four filters and shows just **Status** and **Last run** next to each name; the surrounding window already tells you which agent or channel you are looking at.

## How Jobs Are Created

There is no "Create Job" form in the Jobs app — the list only lets you search, open, and manage existing jobs. You create a job in one of two ways:

- **Ask the assistant in chat.** Tell the built-in Insulin agent what you want to automate (for example, "every weekday at 9am, summarize new AWS entitlements"). The agent creates the job for you, picking the trigger kind that matches what you asked for. See [Chat](/insulin/chat/).
- **Use the REST API.** `POST /jobs` creates a job programmatically.

If you don't specify an agent when creating a job, it runs under the built-in **Insulin** agent. Otherwise it runs under the [agent](/insulin/agents/) you name, with that agent's access to resources and integrations.

:::info
Jobs run within the creating user's scope, so the agent has access to the same resources and integrations that user does. A job is private to the person who created it: **only its owner** can see it, run it, edit it, or delete it. That includes organization administrators — being an org admin does not give you someone else's jobs.
:::

## Triggers

Every job has exactly one of the three trigger kinds above. Manual jobs need nothing else. Cron jobs need a schedule; push jobs need an event source and, optionally, a filter.

### Push sources

A push job subscribes to an event source:

- **Suger events** are the marketplace events Insulin already knows about. When created over REST, the source must match `suger.<entity>.<action>` — for example `suger.OFFER.ACCEPT`. Sources that don't match this pattern are rejected.
- **Third-party events** — GitHub (`github.<event>`), Google Calendar (`calendar.<event>`), and Gmail (`gmail.<event>`) — require a live OAuth connection to that provider and can only be created by asking the assistant, which registers the subscription for you.

A push job's detail page shows its **Source** and, if one is set, its **Filter**.

:::info
Suger event payloads are scrubbed of personal data (emails, secrets, tokens) before the agent sees them or a run record is stored.
:::

## Cron Scheduling

Cron expressions define when a scheduled job runs. Standard cron syntax is supported:

| Expression | Schedule |
|------------|----------|
| `0 9 * * 1-5` | Every weekday at 9:00 AM |
| `0 */6 * * *` | Every 6 hours |
| `0 0 1 * *` | First day of every month at midnight |
| `*/30 * * * *` | Every 30 minutes |

A cron job's detail page shows the raw expression in its header; hover it for a plain-English reading of the schedule. Alongside it are **Last triggered** and **Next** timestamps. **Next** appears only when there is a genuine upcoming run — a disabled or just-re-enabled job has a frozen schedule, so no misleading "next run" is shown.

## Filter Expressions

Filters let you control which events actually trigger an agent run. When an event arrives on a push job — or when a cron tick fires with a payload — the filter expression is evaluated against the event payload. If the filter returns false, the run is skipped and recorded with the **Filtered** status.

Filters use [filtrex](https://github.com/m93a/filtrex) syntax — a safe, sandboxed expression language. The event payload is available under the `event` namespace.

**Examples:**

```
event.source == "aws-marketplace"
```

```
event.amount > 1000
```

```
event.action == "entitlement_created" and event.partner == "AWS"
```

```
lower(event.status) == "active"
```

**Available functions:** `lower()`, `upper()`, `includes()` (checks if an array contains a value).

**Dot notation** is supported for nested fields: `event.buyer.name`, `event.offer.info.commits`.

:::tip
If no filter expression is set, every event passes and triggers an agent run.
:::

## Runs

Every job has **one detail page**, whatever its trigger kind, and every trigger creates a **run** record on it.

The header carries the job's title, its trigger badge, and its state — a run-status badge on a manual job, or an **Active** / **Off** badge on a cron or push job. Below it sit the agent's name and the relevant timestamps.

A **model badge** sits between the agent's name and the timestamps, but only on a **manual** job, and only when that job pins a model. A manual job that inherits its agent's default model shows no badge, and a cron or push job never shows one — so the absence of a badge is not the absence of a model.

Under the header, a manual job shows the outcome of its latest run as a card:

| Card | When it appears | What it holds |
|------|-----------------|---------------|
| **Result** | The latest run completed | The agent's answer, rendered as Markdown. |
| **Error** | The latest run failed | The error message the run ended with. |

Every job then has a **Runs** table with three columns:

| Column | What it shows |
|--------|---------------|
| **Started** | When the run began. |
| **Status** | The run's outcome. |
| **Duration** | How long the run took. |

Expand a row to read that run's result, its error, and — for a push run — the event payload it received. The table pages in more runs as you scroll, and reads *None yet.* until the job has fired for the first time.

Each run ends in one of these statuses:

| Status | Meaning |
|--------|---------|
| **Pending** | The run is queued and waiting to start. |
| **Running** | The agent is currently running. |
| **Completed** | The agent finished successfully. |
| **Failed** | The agent encountered an error. |
| **Cancelled** | An in-flight run was stopped. |
| **Filtered** | The event did not pass the filter expression and was skipped. |
| **Rate limited** | The run was skipped because the job or organization hit its rate limit. |

Filtered and rate-limited events still create a run row, so nothing that fired is invisible.

![A cron job's detail page — the Active and Cron badges beside the schedule, the agent and Last triggered / Next timestamps, and the Runs table with its Started, Status and Duration columns](images/16-job-detail-cron.png)

![A manual job's detail page — the title with its Completed and Manual badges, then the agent and the Created / Started / Done timestamps (this job pins no model, so no model badge sits between them), the prompt, the Result card rendered as markdown, and the Runs table below with one run expanded to show that run's own result](images/29-job-detail-manual.png)

### Rate limits

To keep automations from overwhelming your workspace, runs are rate limited:

| Limit | Default |
|-------|---------|
| Per job | 60 runs per hour |
| Per organization | 300 runs per hour |

Limits apply over a rolling one-hour window. Runs that exceed a limit are recorded with the **Rate limited** status rather than executing.

## Managing Jobs

Open a job from the list to reach its detail page. The action buttons in the header are icons; hover one for its label. Which buttons appear depends on the trigger kind.

**A manual job:**

- **Run** — Start the job. Offered only while the job is pending; a job allows at most one in-flight run at a time, so if a run is already active a second Run is rejected with "Job already has an in-flight run."
- **Terminate** — Stop a run that is currently going. While a run is in flight the header also shows a spinning **Running** indicator.
- **Delete** — Permanently remove the job and its run history. You are asked to confirm by name.

**A cron or push job:**

- **Run now** — Fire the job immediately; a run is created as if its schedule or event had arrived. This works even while the job is switched off, so you can test a paused automation. (The REST trigger endpoint and the assistant also accept an optional payload to exercise the filter against sample data; the in-app **Run now** button fires with an empty payload.)
- **Enable / Disable** — A switch that pauses the job without deleting it. A disabled job never fires on its schedule and shows an **Off** badge instead of **Active**.
- **Delete** — Permanently remove the job and its run history. You are asked to confirm by name.

:::info
There is no edit form in the Jobs app. To change a job's name, schedule, filter, or assigned agent, ask the assistant (it uses an `update_job` action) or send a `PATCH` request to the REST API.
:::

## Use Cases

- **Marketplace monitoring** — A cron job that checks for new entitlements or expiring offers daily and summarizes changes.
- **Automated alerts** — A push job on Suger marketplace events that runs an agent to evaluate severity and notify your team.
- **Recurring reports** — A cron job that runs a data analyst agent weekly to generate revenue or usage reports.
- **Co-sell pipeline tracking** — A cron job that reviews partner referrals on a schedule and flags stale opportunities.
