Skip to content
Talk to our solutions team

Job Definitions

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.

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);
FieldTypeRequiredDefaultNotes
namestringyesJob identity within the tenant
activeboolnotruefalse skips the job entirely
codestringyesThe script
languagestringnojsSee languages below
namespacesstringnoallComma-separated allowlist of script namespaces (e.g. vault,http)
capabilitieslistnononenetwork, filesystem — granted to the sandbox
constraintsmapnomemory (bytes), cpu (shares), network
accessmap or stringnodenied*Authorization rule — {rule, language} or a bare expression
notifications.onsuccessstring or {rule,language}noRuns when the job completes
notifications.onfailurestring or {rule,language}noRuns when the job fails
triggerslistyesSee 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.

Every trigger has a name (which becomes the run’s trigger name) and a type.

triggers:
- name: on-push
type: webhook
path: /hooks/github-push # registered as a POST route

The 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.

triggers:
- name: nightly
type: scheduledrun
schedule: "0 2 * * *" # 5-field cron, or @every 1h / @daily / @hourly / @weekly
context: # optional — becomes the payload
mode: full

Cron is 5-field (no seconds field). The context map is passed to the script as the payload.

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.

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.

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.

capabilities: [network, filesystem] # opt in; default = neither
constraints:
memory: 8000000000 # bytes → cgroup memory limit
cpu: 4 # → cgroup cpu.shares

With 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;