How do I create a job extension?
A job extension is an autonomous work unit — a prompt, an agent, and a trigger — that runs on a schedule or fires on demand without you being in the conversation.
When would I use a job?
Use a job when you want work to happen automatically while you’re away. Good candidates:
- Scheduled briefings — a daily 8am digest of overnight activity, Slack messages, or news
- Health checks — ping your server every hour and write findings to Engram
- Recurring synthesis — weekly summary of everything stored in the knowledge graph
- Chained pipelines — a research job that triggers a formatting job on completion
- Manual triggers — one-tap jobs you fire from the UI but don’t want to re-describe each time
Jobs are not for real-time interactive responses — use a command extension or just chat for that.
How do I create one by chatting?
Describe the schedule and the work. Daemion handles the rest:
You: Create a job that checks my server health every hour
Daemion: I’ll set up a
server-health-checkjob for you.The job is created with a cron trigger of
0 * * * *(top of every hour). It pings your server endpoints, logs the response times and status codes, and stores a summary in Engram. If anything is down, it flags it prominently. The job starts disabled — enable it when you’re ready.
You: Enable it and also write results to a file at ~/logs/health.md
Daemion: Done — job enabled, output now includes
file: ~/logs/health.md. First run is at the top of the next hour.
How do I create one via the API?
Jobs created via the API start with enabled: false unless you explicitly set it. Enable via PATCH /extensions/:id or toggle in the app.
curl -X POST http://localhost:3001/extensions
-H “Authorization: Bearer $DAEMION_TOKEN”
-H “Content-Type: application/json”
-d ’{
“type”: “job”,
“name”: “server-health-check”,
“description”: “Ping server endpoints every hour and store results.”,
“definition”: {
“name”: “server-health-check”,
“description”: “Check server health endpoints and log status.”,
“enabled”: true,
“trigger”: {
“type”: “cron”,
“schedule”: “0 * * * *”
},
“agent”: “haiku”,
“outputs”: [
{ “engram”: true },
{ “file”: ”~/logs/health.md” }
],
“priority”: “normal”,
“max_duration”: 120
},
“source”: “user”,
“enabled”: true
}‘
What does the job schema look like?
Job extensions use GenericDefinitionSchema at the extension envelope level, but the definition field is validated against JobSchema from src/schema/job.ts. The full shape:
// JobDefinition — the shape of definition for type: “job” // Validated against JobSchema in src/schema/job.ts interface JobDefinition { name: string; // kebab-case, matches extension name description: string; // required, min 1 char
enabled: boolean; // default true inside definition; extension.enabled controls runtime
trigger: | { type: “cron”; schedule: string } // standard cron expression, evaluated by croner | { type: “manual” } // never fires on schedule; run via POST /jobs/:name/run | { type: “chain”; after: string }; // fires when the named job completes
agent?: string; // agent name to run the job, e.g. “haiku”, “sonnet”, “opus”
context: Array< | { engram: string } // recall query to run against Engram before the job starts | { read: string } // file path to read and inject into context | { job: string } // output of another job to inject >;
outputs: Array< | { file: string } // write output to this file path | { engram: true } // store output in the knowledge graph | { slack: string } // post to this Slack channel | { chain: string } // trigger this job name after completion >;
chains: string[]; // additional job names to trigger on completion
priority: “low” | “normal” | “high” | “urgent”; // default “normal” max_duration: number; // seconds, default 300 (5 minutes) }
The trigger.schedule field uses standard 5-field cron syntax, evaluated by the
croner
library (see src/core/triggers.ts). The engine calls shouldJobFire() at each heartbeat tick and fires any job whose schedule matches the current time.
Frequently asked questions
trigger: { type: "chain", after: "parent-job-name" }. The engine fires the chained job immediately after the parent completes — not on a clock schedule. Chain triggers are handled directly in src/core/engine.ts after the parent finishes. They do not appear in shouldJobFire() (which handles cron only).haiku for frequent, lightweight tasks (health checks, classification, quick summaries) where cost matters. Use sonnet for research, writing, and moderate reasoning. Use opus sparingly — for judgment-heavy jobs like architectural review or decisions where thoroughness justifies the cost./run/:job with your bearer token to trigger a manual run. Jobs with trigger.type: "manual" only run this way — they never fire on a schedule.minute hour day month weekday. Examples: 0 8 * * 1-5 (8am weekdays), 0 * * * * (every hour), */15 * * * * (every 15 minutes). The croner library is used for evaluation — see its docs for extended syntax support.outputs is an array and all entries are applied. A job can write to a file, store in Engram, post to Slack, and chain another job in a single run. Each output entry is processed after the agent finishes.What can go wrong
400 {"error": "validation failed"} — The definition failed JobSchema validation. Common causes: name doesn’t match kebab-case regex (^[a-z][a-z0-9-]*$), trigger.schedule is missing for a cron trigger, or priority is not one of low | normal | high | urgent.
Job never fires — Check two things: (1) extension.enabled must be true at the extension envelope level, and (2) definition.enabled must be true inside the job definition. Both must be true for the engine to schedule it.
403 {"error": "cannot disable essential extensions"} — Essential built-in extensions cannot be disabled.
401 {"error": "unauthorized"} — Bearer token missing or incorrect. Check ~/.daemion/.gateway-token and ensure $DAEMION_TOKEN is set in your shell.
Chain job fires before parent output is available — Chain triggers fire immediately on parent completion. If your chained job reads the parent’s file output, the file write happens first — but if it queries Engram, allow a brief moment for the Engram store to commit. Use context: [{ "job": "parent-job-name" }] to inject parent output directly rather than re-querying.
max_duration exceeded — Jobs that run longer than max_duration seconds are stopped by the engine. The default is 300s (5 minutes). Increase this for long-running research or synthesis jobs, but keep Haiku jobs under 60s.
What’s next?
- How do I create a command extension? — repeatable chat shortcuts
- How do I create an agent extension? — persistent Claude identities
- Extensions API — full CRUD reference