Job Definitions
Where they live
Section titled “Where they live”Job definitions go in the product repo under jobs/, one or more .yaml files, delivered through meta. They can also be stored as rows in the jobdefinition entity (POST /jobdefinitions), whose definition column holds the same YAML text. Both sources are merged per tenant and reloaded on the next meta refresh.
Structure
Section titled “Structure”The top-level key is jobs: — a list. Each job has a script, optional sandbox settings, an access rule, and one or more triggers.
jobs: - name: cleanup-expired-tokens language: js # default: js access: language: js rule: "true" # fail-closed — only a literal true grants triggers: - name: hourly cleanup type: scheduledrun schedule: "@every 1h" code: | const rows = kisairest("GET", "token?filter=expired"); for (const r of rows.data) kisairest("DELETE", `token/${r.id}`); pluginlog("removed", rows.data.length);Job fields
Section titled “Job fields”| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name | string | yes | — | Job identity within the tenant |
active | bool | no | true | false skips the job entirely |
code | string | yes | — | The script |
language | string | no | js | See languages below |
namespaces | string | no | all | Comma-separated allowlist of script namespaces (e.g. vault,http) |
capabilities | list | no | none | network, filesystem — granted to the sandbox |
constraints | map | no | — | memory (bytes), cpu (shares), network |
access | map or string | no | denied* | Authorization rule — {rule, language} or a bare expression |
notifications.onsuccess | string or {rule,language} | no | — | Runs when the job completes |
notifications.onfailure | string or {rule,language} | no | — | Runs when the job fails |
triggers | list | yes | — | See below |
* A job with no access block is denied unless the service is booted with defaultjobsaccess: true.
Languages: js / javascript (default), v8 (js:v8), lua, go, starlark, cel, expr, wasm. An unrecognized value falls back to JavaScript.
Triggers
Section titled “Triggers”Every trigger has a name (which becomes the run’s trigger name) and a type.
webhook
Section titled “webhook”triggers: - name: on-push type: webhook path: /hooks/github-push # registered as a POST routeThe POST body becomes the job payload. Paths are global and unique — two jobs (or two tenants) cannot claim the same path.
triggers: - name: teams-command type: teams path: /hooks/teams secret: <base64 HMAC key>Requires Authorization: HMAC <base64(hmac_sha256(body, secret))>. The body becomes the payload.
scheduledrun
Section titled “scheduledrun”triggers: - name: nightly type: scheduledrun schedule: "0 2 * * *" # 5-field cron, or @every 1h / @daily / @hourly / @weekly context: # optional — becomes the payload mode: fullCron is 5-field (no seconds field). The context map is passed to the script as the payload.
gitpoll
Section titled “gitpoll”triggers: - name: watch-main type: gitpoll url: https://github.com/acme/repo.git schedule: "@every 5m"Polls the remote on the schedule; fires only when the head commit changes. The payload is empty — read what you need inside the script.
database
Section titled “database”triggers: - name: poll-pending-tasks type: database schedule: "@every 30s" # required — the poll cadence db: dbserver: db.example.com:5432 dbuser: app_user password: vault:tasks-db-pw database: app_db maxconnections: 10 primarykey: id query: "SELECT * FROM tasks WHERE status = 'pending' ORDER BY createdon LIMIT 10" inprogressupdate: "UPDATE tasks SET status='processing' WHERE id = '{{primarykey}}'" successupdate: "UPDATE tasks SET status='completed' WHERE id = '{{primarykey}}'" failedupdate: "UPDATE tasks SET status='failed' WHERE id = '{{primarykey}}'"Each returned row is dispatched as its own run with the row available in the payload. The *update statements are Liquid-templated with {{primarykey}} and mark rows as they move through the lifecycle. Set db.type: clickhouse (with a db.urls list) to poll ClickHouse instead of PostgreSQL. A database trigger requires a schedule — it is the poll interval.
The runtime payload
Section titled “The runtime payload”Whatever a trigger produces is bound into the script as top-level variables — one global per key — plus a pluginlog(...) function for structured logging. For a webhook body {"ref": "main"}, the script reads ref directly:
pluginlog("building ref", ref);The script’s return value is captured as the run’s result. Access rules receive the same top-level bindings plus the caller identity.
Sandbox constraints (Linux)
Section titled “Sandbox constraints (Linux)”capabilities: [network, filesystem] # opt in; default = neitherconstraints: memory: 8000000000 # bytes → cgroup memory limit cpu: 4 # → cgroup cpu.sharesWith capabilities omitted, the sandboxed process has no network and no filesystem access. Sandboxing applies on Linux with cgroups enabled; on other platforms the script runs in-process without these limits.
Worked example — capabilities + notifications + multi-trigger
Section titled “Worked example — capabilities + notifications + multi-trigger”jobs: - name: infra-health-check language: js capabilities: [network] constraints: { memory: 2000000000, cpu: 2 } access: { language: js, rule: "true" } notifications: onfailure: | notify.send("ops-alerts", { text: "infra check failed: " + result }); triggers: - name: every-5-min type: scheduledrun schedule: "@every 5m" - name: manual type: webhook path: /hooks/infra-check code: | const r = http.get("https://status.internal/health"); pluginlog("status", r.status); return r.status;