{"slug":"schedules","title":"Schedules","path":"schedules.md","markdown":"# Schedules\n\nArmed schedules enqueue runner jobs on a fixed **interval** preset (same options as the product UI). Free-form cron is not accepted. Optional webhook hooks are accepted as plaintext JSON on create; the server encrypts them before sending to the runner. Ciphertext is never returned.\n\nSchedule-triggered runs are **not** listed under [tasks](tasks.md). Only one-shot API tasks appear there.\n\n## Interval presets\n\n```bash\nkobold-cli schedules intervals\n```\n\n```http\nGET /v1/schedules/intervals\nAuthorization: Bearer \u003capi_key\u003e\n```\n\n| `id` | Label |\n| ---- | ----- |\n| `every_minute` | Every minute |\n| `every_5_minutes` | Every 5 minutes |\n| `every_25_minutes` | Every 25 minutes |\n| `every_30_minutes` | Every 30 minutes |\n| `every_hour` | Every hour |\n| `daily_09_00` | Daily 09:00 |\n| `weekly_mon_09_00` | Weekly Mon 09:00 |\n\nResponse shape: `{ \"intervals\": [ { \"id\", \"label\" }, … ] }`.\n\n## Schedule object\n\n| Field | Notes |\n| ----- | ----- |\n| `id` | Public `sch_…` |\n| `agentId` | Public `ag_…` |\n| `agentName` | Display name |\n| `knowledgebaseId` | Public `kb_…` when known |\n| `knowledgebaseName` | When resolvable |\n| `prompt` | Prompt text |\n| `interval` | Preset id when the cron matches a known interval |\n| `enabled` | Whether the schedule is armed |\n| `createdAt` / `updatedAt` | RFC 3339 |\n| `lastEnqueuedAt` | RFC 3339, optional |\n\n## List\n\nQuery: `offset`, `limit` (default `50`).\n\n```bash\nkobold-cli schedules list\nkobold-cli schedules list -offset 0 -limit 20\n```\n\n```http\nGET /v1/schedules?offset=0\u0026limit=50\nAuthorization: Bearer \u003capi_key\u003e\n```\n\n```json\n{\n  \"schedules\": [ /* Schedule objects */ ],\n  \"offset\": 0,\n  \"limit\": 50,\n  \"total\": 1\n}\n```\n\n## Create\n\nRequires `agentId`, `knowledgebaseId`, `prompt`, and `interval`. Optional `hooks` with `onComplete` / `onFail` webhook destinations.\n\n```bash\nkobold-cli schedules create -agent ag_7k2m9x4q -kb kb_qc0rp0gz -prompt \"Summarize new files\" -interval daily_09_00\nkobold-cli schedules create -agent ag_7k2m9x4q -kb kb_qc0rp0gz -prompt ./prompt.md -interval every_hour -hooks ./hooks.json\n```\n\n```http\nPOST /v1/schedules\nAuthorization: Bearer \u003capi_key\u003e\nContent-Type: application/json\n\n{\n  \"agentId\": \"ag_7k2m9x4q\",\n  \"knowledgebaseId\": \"kb_qc0rp0gz\",\n  \"prompt\": \"Summarize new files\",\n  \"interval\": \"daily_09_00\",\n  \"hooks\": {\n    \"onComplete\": {\n      \"type\": \"webhook\",\n      \"webhook\": {\n        \"url\": \"https://example.com/hook\",\n        \"headers\": { \"Authorization\": \"Bearer …\" }\n      }\n    },\n    \"onFail\": {\n      \"type\": \"webhook\",\n      \"webhook\": { \"url\": \"https://example.com/fail\" }\n    }\n  }\n}\n```\n\n| Field | Required | Notes |\n| ----- | -------- | ----- |\n| `agentId` | yes | Owner agent from [agents list](agents.md) |\n| `knowledgebaseId` | yes | Knowledgebase `kb_…` |\n| `prompt` | yes | Inline text (CLI also accepts a local file path) |\n| `interval` | yes | One of the preset ids above |\n| `hooks` | no | Only `type: \"webhook\"`; `webhook.url` must be http/https; optional `headers` |\n\nSuccess `200`: a `Schedule`. Unknown agent/KB → `404`. Unknown interval or invalid webhook → `400`. Schedule/task limit → `429`. Upstream/provider failure → `502`.\n\nHooks file for the CLI (`-hooks`):\n\n```json\n{\n  \"onComplete\": {\n    \"type\": \"webhook\",\n    \"webhook\": { \"url\": \"https://example.com/hook\", \"headers\": { \"Authorization\": \"Bearer …\" } }\n  },\n  \"onFail\": {\n    \"type\": \"webhook\",\n    \"webhook\": { \"url\": \"https://example.com/fail\" }\n  }\n}\n```\n\n## Get\n\n```bash\nkobold-cli schedules get -id sch_7k2m9x4q\n```\n\n```http\nGET /v1/schedules/sch_7k2m9x4q\nAuthorization: Bearer \u003capi_key\u003e\n```\n\nUnknown or other-tenant id → `404`.\n\n## Enable / disable\n\n```bash\nkobold-cli schedules enable -id sch_7k2m9x4q\nkobold-cli schedules disable -id sch_7k2m9x4q\n```\n\n```http\nPOST /v1/schedules/sch_7k2m9x4q/enable\nAuthorization: Bearer \u003capi_key\u003e\n```\n\n```http\nPOST /v1/schedules/sch_7k2m9x4q/disable\nAuthorization: Bearer \u003capi_key\u003e\n```\n\nSuccess returns the updated `Schedule`.\n\n## Delete\n\n```bash\nkobold-cli schedules delete -id sch_7k2m9x4q\n```\n\n```http\nDELETE /v1/schedules/sch_7k2m9x4q\nAuthorization: Bearer \u003capi_key\u003e\n```\n\nSuccess: `{}` / CLI `{\"ok\": true}`.\n\n## Webhook signing\n\nSchedule hooks can be authenticated with a **Standard Webhooks** HMAC secret. Signing is account-level (one secret for all of your schedule destinations), not per URL. There is no public API to create or fetch the secret.\n\n### Configure the signing secret\n\n1. Open the product dashboard → **Settings** → **Security**.\n2. Choose **Generate secret** (or **Rotate secret** if one already exists).\n3. Copy the `whsec_…` value immediately and store it in your receiver’s config (for example an environment variable). The plaintext is shown **once**, same as API keys.\n4. Use **Delete** to revoke the secret. Until you generate a new one, hooks are sent unsigned again.\n\nUntil a secret is configured, hooks still POST to your URL with the JSON body and any custom headers you set — they are simply unsigned. After a secret is configured, every schedule webhook from that account is signed.\n\n### How signing works\n\nOutbound POSTs use symmetric HMAC (Standard Webhooks), not public/private keys.\n\n| Header | Value |\n| ------ | ----- |\n| `webhook-id` | Unique message id (`msg_…`) |\n| `webhook-timestamp` | Unix seconds |\n| `webhook-signature` | One or more `v1,\u003cbase64\u003e` entries (space-delimited) |\n\n- Secret format: `whsec_` + base64(32 random bytes).\n- Signed content: `{webhook-id}.{webhook-timestamp}.{raw_json_body}` (exact bytes of the request body).\n- Signature: strip the `whsec_` prefix, base64-decode the remainder, compute HMAC-SHA256, base64-encode the digest, prefix with `v1,`.\n- Receivers should reject timestamps more than about **5 minutes** from current time (replay protection).\n- Custom destination `headers` from the schedule still apply, but they never overwrite the three signature headers above.\n\n### Verify (any language)\n\n1. Read the **raw** request body (do not re-serialize JSON before verifying).\n2. Require `webhook-id`, `webhook-timestamp`, and `webhook-signature`.\n3. Reject if `|now - timestamp|` is greater than ~5 minutes.\n4. Decode the secret (strip `whsec_`, base64-decode).\n5. HMAC-SHA256 the string `{id}.{timestamp}.{body}`; accept if any space-separated entry in `webhook-signature` matches `v1,\u003cbase64\u003e` (constant-time compare).\n\n### Verify in Go\n\n```go\nimport (\n    \"net/http\"\n    \"os\"\n\n    \"github.com/InsulaLabs/web-api/api\"\n)\n\nfunc handle(w http.ResponseWriter, r *http.Request) {\n    wh, err := api.ParseIncomingWebhook(r)\n    if err != nil {\n        http.Error(w, err.Error(), http.StatusBadRequest)\n        return\n    }\n    if err := wh.Validate(os.Getenv(\"KOBOLD_WEBHOOK_SECRET\")); err != nil {\n        http.Error(w, err.Error(), http.StatusUnauthorized)\n        return\n    }\n    // wh.Body / wh.Payload\n}\n```\n\n`Validate` implements the checklist above (missing headers, skew window, and HMAC).\n\n## Related\n\n- [Agents](agents.md)\n- [Tasks](tasks.md) — one-shot runs only\n- [CLI reference](cli-reference.md)\n- [Errors](errors.md)\n"}
