Jobs
A job is a prompt you leave behind. You ask the agent for it once, in a chat, and from then on it runs on its own: no conversation, no one watching, and, if the job was created with a notification requested, a message when it has something to say.
Jobs are how q15 does the work you would otherwise have to remember to ask for. A morning summary of a folder, a check on a page that changes, a weekly tidy-up of a workspace.
What a job costs you
Section titled “What a job costs you”Read this before you create a dozen of them.
- Model calls. Every run is real turns against a real model, on your provider account. A job with no tools is cheap; a job that reads twelve files and searches the web is not.
- Five minutes, then it is killed. A run is stopped after five minutes of wall time. That cap is not configurable through the agent config today; the field request is filed.
- A tool allow-list decided at creation. A job can use only the tools named when it was created or last updated. That is a security property, not a convenience: a job that should read a file has no reason to hold your shell.
- Ownership. A job belongs to the chat that created it. Create it in Telegram and it reports in Telegram; it is not visible from the browser client, and the browser client cannot run it.
Create one
Section titled “Create one”You do not write a file. You ask, in chat:
Every weekday at 07:00 Berlin time, summarise anything new in
/workspace/inboxand send me three lines. Do not reply if there is nothing new.
The agent calls schedule_create, which takes:
| Field | What it means |
|---|---|
name |
The label you will recognise in a list. |
prompt |
The task, written for an isolated agent that has no memory of this conversation. |
kind |
oneshot or recurring. |
run_at |
An RFC3339 UTC timestamp. Required for a one-shot. |
cron |
A strict five-field UTC cron expression. Required for a recurring job. |
notify |
Whether the run reports back. A silent job still writes its record. |
model |
Optional provider and model ref to pin the run. Omitted, it uses the model you are on when you create it. |
max_turns |
Optional turn cap for a run, bounded by the deployment. |
allowed_tools |
The exact tool names the job may use. An empty list means it needs none. |
context_profile |
minimal (default) or agent. See below. |
Context profiles. minimal is the default and the one you want most of the time: the job gets its
prompt and its tools, and nothing of your conversation. agent adds the stable agent context (core
memory and the skill catalog) but still never the interactive conversation. Neither profile changes
which tools the job may use; only allowed_tools does.
Local time is your problem, not the scheduler’s: both run_at and cron are UTC. If you think in
Berlin time, subtract the offset, and remember it moves twice a year.
Limits
Section titled “Limits”| Limit | Default | Where it comes from |
|---|---|---|
| Jobs per agent | 64 | agent.tools.schedule.max_jobs, bounded at 1000 |
| Turns per run | 16 | agent.tools.schedule.max_run_turns, bounded at 128 |
| Wall time per run | 5 min | Fixed. Not in the YAML. |
| Concurrent runs | 1 | A job that is already running is skipped when its next slot arrives. |
Reaching max_jobs fails the create rather than silently dropping the oldest job.
Watching them
Section titled “Watching them”From chat:
schedule_list— the jobs this chat owns, with their next fire time and last status.schedule_runs— the archived history: exact counts by status, and the newest records, filtered by job id, name, status or time window. Use it when you want to know whether the thing actually ran.schedule_update— change the prompt, the schedule, the tools or the model without changing the job’s identity or its history.schedule_delete— remove it. Its run records stay.
On disk, everything is a file you can read:
/var/lib/q15/agent/schedule/├── jobs/<job-id>.json one file per job: prompt, schedule, tools, last status└── runs/YYYY/MM/DD/<time>-<run-id>.json one record per run: model, tools, start, finish, status, output, errorThe records are the audit trail. last_run_at, last_success_at, consecutive_failures and
last_error sit on the job itself.
When a run fails
Section titled “When a run fails”- The model is gone. If the pinned model is no longer in the roster, the run is skipped and the refusal is reported once, not on every fire, until the model comes back or you re-pin the job.
- Ordinary failure. The run is recorded with its error and
consecutive_failuresclimbs. You are told, if the job asked to be. A job is never disabled automatically: a failing job keeps failing until you look. - A crash mid-run. The durable record is written before the notification is delivered, so a crash can duplicate a notification but cannot lose the run or replay it.
What jobs are good for
Section titled “What jobs are good for”- A digest: “what changed in these folders since yesterday”.
- A watch: “tell me when this page changes in a way that matters”.
- A rhythm: “every Monday, tidy
/workspace/scratchand report what you removed”. - A reminder with teeth: “on the 20th, check whether the invoice in
/workspace/financewas paid”.
Not good for: anything that needs more than five minutes, anything that needs a conversation, or anything you would not want a slightly confused agent doing unattended with the tools you gave it.