---
title: Hermes scheduled job capabilities
description: >-
  Create and govern persisted Hermes jobs without confusing an accepted
  scheduling request with completed execution or delivery
kind: capability-reference
audience: operator
appliesTo: "1.x"
writingStandard: "aip-docs/1.0"
lastReviewedRevision: "97be86e9efedf07ecf1783b03800f683f107fb04"
connector: hermes-agent
capabilityIds:
  - cap:hermes_agent:<endpoint_id>:jobs_list
  - cap:hermes_agent:<endpoint_id>:job_create
  - cap:hermes_agent:<endpoint_id>:job_get
  - cap:hermes_agent:<endpoint_id>:job_update
  - cap:hermes_agent:<endpoint_id>:job_delete
  - cap:hermes_agent:<endpoint_id>:job_pause
  - cap:hermes_agent:<endpoint_id>:job_resume
  - cap:hermes_agent:<endpoint_id>:job_run
---

# Hermes scheduled job capabilities

Use these eight endpoint-qualified capabilities to manage jobs in one Hermes
profile's persistent scheduler.

An accepted AIP action confirms that Hermes stored or changed scheduler state.
It does not confirm that a job ran, produced output, or delivered a message.
In particular, `job_run` only makes a job due for later scheduler execution.

A scheduled job can invoke an agent, tools, scripts, providers, and delivery
adapters configured outside this API. Inspect a job before approving any
operation that creates, changes, resumes, runs, or deletes it.

## Operations

Replace `<endpoint_id>` with the normalized endpoint ID returned by AIP
discovery.

| Operation | Provider request | Kind | Risk | Approval | Key | Retry |
|---|---|---|---|---|---|---|
| `jobs_list` | `GET /api/jobs` | Tool | Low | No | Optional | Safe |
| `job_create` | `POST /api/jobs` | Tool | Medium | Yes | Required | Unsafe |
| `job_get` | `GET /api/jobs/{job_id}` | Tool | Low | No | Optional | Safe |
| `job_update` | `PATCH /api/jobs/{job_id}` | Tool | Medium | Yes | Required | Unsafe |
| `job_delete` | `DELETE /api/jobs/{job_id}` | Tool | High | Yes | Required | Unsafe |
| `job_pause` | `POST /api/jobs/{job_id}/pause` | Tool | Medium | Yes | Required | Unsafe |
| `job_resume` | `POST /api/jobs/{job_id}/resume` | Tool | Medium | Yes | Required | Unsafe |
| `job_run` | `POST /api/jobs/{job_id}/run` | Agent | High | Yes | Required | Unsafe |

All eight capabilities are stable. They require the selected endpoint's
Bearer credential. Input cannot select a different endpoint or provider URL.

Every path operation requires a 12-character lowercase hexadecimal `job_id`,
for example `a1b2c3d4e5f6`. A name, uppercase ID, shortened ID, or arbitrary UUID
is rejected by the connector before the provider call.

## Choose a schedule

Hermes parses four schedule forms.

| Intent | Example | Stored kind | First eligible time |
|---|---|---|---|
| Run once after a delay | `30m`, `2h`, `1d` | `once` | Creation time plus the delay |
| Run at an interval | `every 30m`, `every 2h` | `interval` | Creation time plus the interval |
| Follow a cron expression | `0 9 * * *` | `cron` | Next matching occurrence |
| Run once at a timestamp | `2030-01-15T09:00:00+04:00` | `once` | The supplied instant |

A timestamp without an offset is interpreted in the configured Hermes
timezone. Supply an explicit offset when an application must preserve one
unambiguous instant across deployments.

A one-shot more than 120 seconds in the past is rejected. A one-shot created
without `repeat` receives a run limit of one. Use recurring interval or cron
schedules for repeated work.

Cron expressions depend on the provider's `croniter` installation. The pinned
Hermes source treats it as a runtime dependency, but an unavailable dependency
still makes expression parsing or next-run computation fail.

## Shared AIP contract

| Contract field | List and get | Create and update | Pause and resume | Delete | Run |
|---|---|---|---|---|---|
| Side effects | `read`, `external_network` | `read`, `write`, `external_network` | `read`, `write`, `external_network` | `read`, `delete`, `external_network` | `read`, `write`, `external_network` |
| Published data class | Internal; no PII | Confidential; contains PII | Internal; contains PII | Restricted; contains PII | Confidential; contains PII |
| Redaction | Required | Required | Required | Required | Required |
| Idempotency | Optional | Required | Required | Required | Required |
| Retry support | Yes | No | No | No | No |
| Retry safety | Safe | Unsafe | Unsafe | Unsafe | Unsafe |
| Completion | Sync | Sync | Sync | Sync | Sync request; delayed job |
| Compensation | Rollback not supported | Rollback not supported | Rollback not supported | Rollback not supported | Rollback not supported |

All eight contracts declare a 60-second timeout, 5-second expected latency,
10-second maximum queue-delay hint, and `99.9%` availability target. The
connector applies a 30-second HTTP timeout to each JSON request. Trusted
execution is also bounded by the AIP deadline and cancellation token.

These values describe the control request only. They do not bound the later
job execution. The availability value is a declaration, not observed service
or qualification evidence.

The six mutations require tenant-policy approval. Approval lasts 15 minutes
and records a reason, input snapshot, and policy decision. No operation
publishes transaction support, and no compensation capability is available.

The `job_run` contract understates possible downstream effects. A stored job
can later send a message or execute code even though the capability declares
only read, write, and external-network effects. Base policy on the retrieved
job and the endpoint's Hermes configuration, not that generic list alone.

## List jobs

`jobs_list` returns enabled jobs by default:

```json
{}
```

Include paused and otherwise disabled records when reconciling scheduler state:

```json
{
  "include_disabled": true
}
```

The provider returns one object containing a `jobs` array. There is no limit,
cursor, offset, total count, or documented sort. Results retain storage order.

The connector also accepts a `query` object and forwards scalar values. Prefer
the typed top-level `include_disabled` field. Supplying the same key in both
places creates duplicate query parameters with provider-dependent selection.

The response contains full normalized job records rather than a safe summary.
A record can include its prompt, skills, model and provider routing, delivery
target, origin network metadata, schedule, timestamps, errors, and execution
state. Jobs created by other Hermes surfaces can expose fields that AIP cannot
set.

The list is profile-scoped, not caller-scoped. It covers every job in the
selected endpoint's active `HERMES_HOME`, including jobs created outside AIP.

## Create a job

Use top-level fields for the frozen AIP schema:

```json
{
  "name": "Reconcile support queue",
  "schedule": "every 30m",
  "prompt": "Summarize unresolved queue items and record the totals.",
  "deliver": "local",
  "skills": ["support-reporting"],
  "repeat": 24
}
```

| Field | Requirement | Frozen AIP bound |
|---|---|---|
| `name` | Required, non-empty string | 200 characters |
| `schedule` | Required, non-empty string | 512 characters |
| `prompt` | Optional string | 5,000 characters |
| `deliver` | Optional string | 256 characters |
| `skills` | Optional string array | 256 items, 256 characters each |
| `repeat` | Optional positive integer | At least one |

The schema rejects other top-level fields. Although a `body` property exists
for transport compatibility, mixing it with typed fields can cause the wrapper
to replace those fields before the provider call. Use the typed form above.

The provider trims `name` and `schedule`, parses the schedule, and generates a
12-hex job ID. When the optional prompt scanner imports successfully, it also
rejects blocked patterns in a non-empty prompt. An API-created job records
`api_server` origin metadata and defaults delivery to `local`.

Hermes stores the initial job as enabled with state `scheduled`. It calculates
`next_run_at`, initializes run counts and status, and returns:

```json
{
  "job": {
    "id": "a1b2c3d4e5f6",
    "name": "Reconcile support queue",
    "schedule": {
      "kind": "interval",
      "minutes": 30,
      "display": "every 30m"
    },
    "schedule_display": "every 30m",
    "repeat": {
      "times": 24,
      "completed": 0
    },
    "enabled": true,
    "state": "scheduled",
    "last_run_at": null,
    "last_status": null,
    "last_error": null,
    "last_delivery_error": null,
    "deliver": "local"
  }
}
```

The actual response contains more fields. Treat the returned ID as the only
valid identifier for later AIP operations.

Put the required idempotency key on the AIP Action, not in this JSON. The
connector forwards it as `Idempotency-Key`; the pinned provider handler does
not interpret that header.

## Inspect one job

Use `job_get` before every governed mutation:

```json
{
  "job_id": "a1b2c3d4e5f6"
}
```

The provider returns `{"job": <record>}` or `404` when the ID is absent.
Inspect at least these fields:

- `enabled`, `state`, `next_run_at`, and `schedule` for eligibility;
- `prompt`, `skills`, and any script or toolset fields for execution scope;
- `provider`, `model`, and delivery fields for external destinations;
- `repeat`, `last_run_at`, `last_status`, and errors for prior outcomes.

An origin record is audit context. It is not proof that the current caller
created or owns the job.

## Update safe fields

`job_update` accepts a path ID and a partial body:

```json
{
  "job_id": "a1b2c3d4e5f6",
  "name": "Reconcile priority support queue",
  "schedule": "every 1h",
  "prompt": "Summarize unresolved priority items and record the totals."
}
```

The provider whitelists `name`, `schedule`, `prompt`, `deliver`, `skills`,
legacy `skill`, `repeat`, and `enabled`. Other provider fields are discarded.
An effective patch with no whitelisted field returns `400`.

A changed schedule is reparsed. If the job is not paused, Hermes recalculates
its next eligible time. A non-empty changed prompt is scanned again.

Do not patch `repeat` in the pinned version. The REST handler forwards an
integer, but the scheduler later expects an object with `times` and
`completed`. This source-level shape mismatch can leave the job unreadable to
execution paths. To change the limit:

1. Pause and inspect the old job.
2. Create a replacement with the intended schedule and `repeat` value.
3. Inspect the replacement and then delete the old job.

Do not use a direct `enabled` patch as a substitute for pause or resume. It
does not maintain `state`, `paused_at`, `paused_reason`, and next-run timing as
one lifecycle operation.

As with create, use typed top-level fields instead of combining them with a
`body` wrapper. The connector removes `job_id` and transport-only fields from
the provider body.

## Pause and resume

Pause an enabled job with only its ID:

```json
{
  "job_id": "a1b2c3d4e5f6"
}
```

`job_pause` sets `enabled` to false, state to `paused`, records `paused_at`,
and returns the updated job. This AIP route has no pause-reason input, so the
provider stores no caller-supplied reason.

`job_resume` clears pause metadata, enables the job, and recomputes
`next_run_at` from the time of the request. Resuming an active job can shift
its next run, so call it only for a record whose state you just verified as
paused.

An expired one-shot cannot be resumed. The provider returns an error when its
stored time is beyond the one-shot grace window. Create a replacement instead
of retrying the same resume request.

## Trigger a run

`job_run` uses the same ID-only input. Despite its name, the provider does not
execute the job in the HTTP request. It performs this state transition:

```json
{
  "enabled": true,
  "state": "scheduled",
  "paused_at": null,
  "paused_reason": null,
  "next_run_at": "<provider-current-time>"
}
```

It then returns `{"job": <updated-record>}`. The later scheduler owns model or
script execution, output storage, delivery, and final status.

The built-in gateway ticker normally checks every 60 seconds. A configured
external scheduler can use different timing. Unlike create, update, delete,
pause, and resume, the pinned `job_run` handler does not send the best-effort
jobs-changed notification to an external scheduler provider. Verify its
behavior separately before promising immediate execution on that topology.

Treat a successful `job_run` response as “made due,” not “ran successfully.”
For a recurring job, poll `job_get` until `last_run_at` advances, then inspect
`last_status`, `last_error`, and `last_delivery_error`.

A finite one-shot can be removed automatically when its run limit is reached,
so `404` after dispatch is not proof of either success or failure. Correlate it
with retained output, delivery, and endpoint operational evidence. The AIP job
family has no endpoint for reading saved run-output files.

## Delete a job

`job_delete` removes the selected record and its per-job output directory. A
successful provider response is:

```json
{
  "ok": true
}
```

Deletion cannot recall messages already delivered, reverse tool or script
effects, remove data written elsewhere, or prove that an in-flight execution
was cancelled. Pause first, verify that no run is active through operational
signals, and then delete.

The provider resolves and validates the output path before saving the record
removal. A legacy unsafe ID therefore fails closed instead of partially
deleting scheduler state. AIP only accepts current 12-hex IDs.

Record removal and output cleanup are not one transaction. Hermes saves the
record removal before deleting the output directory. If that filesystem step
fails, the provider can return `500` after the job is already absent and leave
output behind. Re-list before deciding whether any cleanup remains.

## Persistence and execution boundary

Jobs are stored under the active profile at `HERMES_HOME/cron/jobs.json`.
Writes use a temporary file plus atomic replacement. Directories and files are
set to owner-only permissions where the operating system supports them.

Mutations use an in-process lock and a best-effort cross-process advisory lock.
If the advisory lock cannot be opened, Hermes logs a warning and continues
with in-process protection. This is not a distributed database or a
cross-instance consensus boundary.

The built-in scheduler prevents overlapping dispatch of the same job in one
runtime and advances recurring schedules before execution. One-shots use
durable run claims and repeat accounting. These controls reduce duplicate
dispatch; they do not establish exactly-once model, tool, script, or delivery
effects across every crash and external system.

Each execution writes a Markdown output file under
`HERMES_HOME/cron/output/<job_id>`. The pinned default keeps the newest 50
files per job. Output is saved before delivery. Execution failure and delivery
failure are recorded separately.

## Protect job data and authority

Treat every job record as Confidential and potentially containing PII. The
published AIP contract labels list/get results Internal with `contains_pii`
false, but real records can disclose prompts, network origin, delivery
addresses, errors, model routing, skills, and operational timestamps.

Provider handlers authenticate the endpoint Bearer token. They do not perform
per-job principal, creator, or tenant ownership checks. An AIP tenant binding
can restrict which endpoint a tenant reaches, but it does not add row-level
authorization inside the shared profile store.

Use a separate endpoint and `HERMES_HOME` for each isolation domain, or add an
authorization layer that filters job IDs and list results. Never expose an
unfiltered shared-profile job catalogue to mutually untrusted principals.

Redact prompts, origins, delivery targets, provider details, and errors from
logs and support bundles. Store endpoint credentials outside action input.

## Idempotency and retry decisions

Required mutation keys are scoped to the full endpoint-qualified capability
ID and retained by the AIP runtime for 24 hours. Reusing a key returns the
original AIP result. The provider itself does not enforce the forwarded
`Idempotency-Key` header.

The replay record does not revalidate an input hash. Reusing the same key with
a different job ID or body can return the earlier result without applying the
new intent. Use a new key whenever the selected job or mutation changes.

| Situation | Decision |
|---|---|
| List/get failed before a response | Retry within the trusted deadline |
| Mutation returned a definitive provider response | Do not retry with a new key |
| Mutation timed out with the original AIP action unresolved | Reconcile with list/get before any new action |
| `job_run` returned success | Poll status; do not submit another run immediately |
| Resume rejected an expired one-shot | Create a replacement with a new key |
| Repeat limit must change | Pause, recreate, verify, and delete; do not patch `repeat` |

An AIP timeout or cancellation can race a provider mutation that already
persisted. Read current state before deciding whether compensation is needed.

## Failures

| Failure | Meaning | Operator response |
|---|---|---|
| Local invalid-input error | Missing or malformed 12-hex ID, query, or body | Correct input; no provider request occurred |
| `401` or `403` | Endpoint credential absent, invalid, or denied | Repair endpoint authentication |
| `400` | Invalid ID, field, prompt, repeat, or effective update | Correct the request; do not replay unchanged |
| `404` | Job does not exist in this profile | Re-list including disabled jobs |
| `501` | Hermes cron module is unavailable | Repair the endpoint installation |
| `500` | Parsing, storage, scheduler, or provider-side handler failure | Reconcile state; inspect redacted endpoint logs |
| AIP deadline or cancellation | Trusted execution stopped waiting | Reconcile before issuing a new mutation |

Schedule parsing exceptions can surface as provider `500` in the pinned REST
handler rather than a more specific `400`. Read the redacted error text and
the submitted schedule before treating it as an infrastructure outage.

## Verify an integration

Before enabling production scheduling, verify from source and controlled
endpoint evidence that:

1. Discovery exposes exactly eight job capabilities for the intended endpoint.
2. Tenant binding and the endpoint Bearer credential fail closed.
3. IDs are returned by create and never guessed from names.
4. List without `include_disabled` hides a paused job; list with it shows one.
5. Resume recalculates timing and an expired one-shot fails visibly.
6. `job_run` returns before execution and status changes only after a tick.
7. Execution failure and delivery failure remain distinguishable.
8. A repeated AIP mutation key returns the original result.
9. A changed mutation uses a new key and a fresh approval decision.
10. Repeat limits are changed only through replacement in this pinned version.
11. Job records and output files follow the intended profile isolation.
12. Delete removes local record/output but is not treated as effect rollback.

Do not report live qualification from this source-only checklist. Record actual
environment, revision, time, and retained artifacts on the qualification page.

## Related documentation

- [Hermes capability index](README.md)
- [Durable runs and approvals](durable-runs-and-approvals.md)
- [Persistent sessions](sessions.md)
- [Approvals and policy](../../../concepts/approvals-and-policy.md)
- [Errors and retry decisions](../../../reference/errors.md)
- [Authentication and endpoints](../getting-started/authentication-and-endpoints.md)
