Concepts
Jobs, workflows, and webhooks
Official Kody doc
Everything your agent builds in Kody can run without the agent. That is the difference between an automation and a chat transcript. Kody has four ways to start work when nobody is typing, and every one of them runs a package you own — no model in the loop, no tokens spent.
| You want to run… | Use |
|---|---|
| The same thing on a schedule | a job |
| One thing later, or something longer than a request | a workflow |
| Something when Sentry, GitHub, Stripe, or a CLI POSTs | an inbound webhook |
| Something when Kody itself emits an event | a subscription |
Prefer the event that actually describes the moment — a webhook or a subscription — and reach for a schedule when no event exists. Cron is fine; it is just not the hero.
Jobs — recurring schedules that travel with the package
A job is declared in the package manifest, so the schedule ships with the behavior it runs:
{
"kody": {
"jobs": {
"daily-digest": {
"entry": "./src/daily-digest.ts",
"schedule": { "type": "cron", "expression": "0 8 * * *" },
"timezone": "America/Denver",
"enabled": false
}
}
}
}- The entry is a package-local module with no arguments. The usual shape is a thin wrapper that calls a callable export and sends notify-self mail only when there is something to say.
- Publish with
"enabled": false, invoke the wrapper once fromexecuteto prove it, then enable. A schedule you have never run once is a schedule you will debug at 8 a.m. - Each run gets a job-scoped scratch bucket; shared durable state (cursors, the
last seen id) belongs in
packageStorage(). jobUpdateadjusts schedule, timezone, enabled state, params, andexpires_at;jobRunNowfires one run for debugging. Name and source stay in the repo — change them there and publish.
Jobs do not hang off an integration or an MCP connection. If a "job" has no
package, save the package first. Runs show up on /account/jobs.
Workflows — deferred and durable one-shot work
A workflow is a durable run Kody executes later, outside the request that asked
for it. Use one instead of plain execute for:
- later —
runAtin the future ("remind me Friday", "retry after the window") - longer — batch sweeps, migrations, polling loops, or steps that would exceed execute's ~90 second budget; workflow steps get a longer sandbox
- once — an
idempotencyKeymakes a repeatedcreatereturn the existing run instead of starting a duplicate
import { workflows } from 'kody:runtime'
export default async function main() {
return await workflows.create({
exportName: './rebuild-index',
runAt: new Date(Date.now() + 15 * 60_000),
idempotencyKey: 'rebuild-index:2026-09-08',
})
}workflows.create takes either code (a complete module string) or
exportName (a saved-package export). Inspect runs with workflowRunList,
cancel with workflowRunCancel; they appear on /account/workflows. Recurring
work is a job, not a workflow that reschedules itself.
Inbound webhooks — the external HTTP knock
A webhook gives a package a public, credentialed URL. A provider POSTs to it and Kody dispatches the validated request to the package export that owns it.
- Declare it under
package.json#kody.webhooks: aname, theexportit binds to, and (for vendor senders)verification— HMAC header, encoding, and the name of the signing secret in your secret store. One webhook name binds one export; there is no wildcard. - Store the signing secret with
secretSetunder that name. - Mint the URL with
webhookUrlMintand paste it into the provider. The URL secret is returned only on mint or rotate — treat the URL as a credential.
Declaring a webhook does not open ingress; minting does. Deliveries are rate-limited per webhook (default 60 per minute, at most 600), and body-only HMAC is replayable unless you opt into a timestamp window or delivery-id header. Full contract and payload shapes: Inbound webhooks.
Sentry noise, GitHub events, Stripe payments, a shortcut on your phone, a CLI on your laptop — anything that can POST JSON can start a package this way.
Subscriptions — events from inside Kody
When the trigger is something Kody already knows about — a message landing in
your inbox, a repo push, a run error, an integration losing auth — a package
subscribes to that topic in package.json#kody.subscriptions and Kody invokes
the handler with the event payload. packageSubscriptionDispatch sends a
synthetic event so you can test a handler before the real one arrives. Topics,
payloads, and package-emitted events:
Subscriptions and events.
The inbox is a trigger too
Every account has an email address. Mail that arrives there is stored and emits
email.message.received, so "forward it to Kody" is a valid trigger for people
and systems that can send email but cannot call an API. The same inbox is how a
job tells you it finished: emailSend mails the account's own address and
nothing else. See Email primitives.
Choosing well
- A schedule is optional, not the point. If the person cannot name a time they want something to happen, leave the trigger off and let them ask.
- Test before you enable: invoke the wrapper from
execute, dispatch a synthetic event, or send yourself one webhook. - Keep the wrapper quiet. Notify only when there is news; an empty digest every morning trains people to ignore the real one.
- Failures and recent runs for every trigger live on
/account/activity.
Where to go next
- Package lifecycle — testing a scheduled wrapper before enabling its schedule.
- How Kody works — one loop from ad hoc question to a daily email that stays quiet until something ships.
- Workflows — the full
workflows.createreference.