Execute and workflows
September 20, 2026 · View on GitHub
execute runs one ephemeral ES module inside Kody's runtime. That module uses normal imports and exports and must default export the entry function Kody should invoke.
Shape of the code
Author code as one module string. Export a default function. On execute,
name that argument params so it mirrors execute({ code, params }):
export default async function main(params) {
// Use params directly, or pass them to shared helpers.
}
Import runtime APIs from kody:runtime when you need Kody helpers. These
helpers are runtime exports:
- use
import { kody } from 'kody:runtime'to call builtin capabilities discovered by search asawait kody.capabilityId(params) - use
import { createAuthenticatedFetch, oauthClientCredentials } from 'kody:runtime'for OAuth helpers - use
import { secretHeaders } from 'kody:runtime'when an approvedfetchrequest needs host-derived auth headers from saved secrets, such as Basic Auth from a saved client id and client secret - use
import { packageStorage } from 'kody:runtime'inside saved-package code for the package's own bucket — always, in every package surface; see Package storage - use
import { workflows } from 'kody:runtime'to queue Cloudflare Workflows from execute calls, package jobs, package subscriptions, package apps, and package exports. Prefer workflows for durable batch sweeps, migrations, polling loops, retryable steps, deferred one-shot work, or work that may run longer than execute's timeout. See Workflows - use
import { packageContext } from 'kody:runtime'inside saved package code when you need package metadata; it isnullfor ad hoc execute calls - when a package name is data, use
import(specifier)for a caller-owned or forked module. Exactly-once work uses workflows. When the target package's name is known when the code is written, use a statickody:@...import (see below) - use
import thing from 'kody:@scope/my-package/export-name'orimport { helper } from 'kody:@scope/my-package/export-name'to reuse a saved package export by npm-scoped package name. This is the default for package reuse whenever the target package's name is known when the code is written: static imports are publish-verified (repo checks prove the export exists), dependency-graph-visible (kody.dependencies, dependents tracking), and have zero per-call platform cost. Ad hoc execute bundles per call, so static imports from execute always see the current published version; snapshot staleness only affects package-to-package static dependencies, which keep the bundled snapshot until the dependent republishes.
kody:runtime is always supplied by the Kody host at execution time. Published
package artifacts do not bundle the host runtime; execution always hydrates the
deployed kody:runtime module.
Prefer a static kody:@... import when the target package's name is known when
the code is written. Use import(specifier) when the name is data.
execute also accepts optional params. Kody passes that JSON object to
the module's default export as the first function argument. Shared helpers
should receive that object through normal function arguments.
When the execute-invoke experiment is on for the caller (operators enable that
flag for the experiments_opt_in audience at /account/experiments),
execute also accepts invoke: a package export specifier such as
kody:@scope/package/export or @scope/package#export. invoke is mutually
exclusive with code. Kody writes the same canonical thin passthrough a careful
agent would write (import action from "kody:@scope/package/export" plus a
default export that calls it with params) and then runs the ordinary execute
path. The Dynamic Worker identity matches that hand-written module. Arbitrary
URLs are rejected. Vary args via params.
Ad hoc worker identity follows the acting user plus that code module
graph. Put varying capability args in params, not string literals inside
code, so the same graph is reused for the UTC day.
Bad (new isolate per distinct literal):
import { kody } from 'kody:runtime'
export default async function main() {
return await kody.emailSend({
subject: 'Hello from Kody',
text: 'Notify-self mail from an execute module.',
})
}
Good (same graph reused; vary via params):
import { kody } from 'kody:runtime'
export default async function main(params) {
return await kody.emailSend(params)
}
// execute({ code, params: { subject: 'Hello from Kody', text: 'Notify-self mail from an execute module.' } })
Top-level await is acceptable when needed.
Server-Timing phases
Execute responses include Server-Timing-style phase entries under
timing.serverTiming (public tool) or top-level serverTiming
(meta.execute): each entry is { name, durationMs }.
bundle— module-graph preparation and bundling.hydrate— refreshing nested runtime modules, including compatibility placeholders retained in already-published bundle snapshots.provider-assembly— capability registry, runtime helper, and provider wiring ahead of sandbox startup.sandbox— the dynamic worker evaluation of the module itself.run— the enclosing span for the three phases above (plus run-record and usage bookkeeping). Phase durations are measured withDate.now(), which the Workers runtime only advances across I/O boundaries — synchronous CPU inside one phase can be attributed to the next timer read. Treat phases as attribution for slow spans, not precise CPU accounting.
npm packages on Workers
execute and saved packages may import npm packages directly when they are
compatible with the Cloudflare Workers runtime. Prefer existing packages over
rewriting helpers; useful starting points include p-retry, mailparser,
remark / mdast-util-to-markdown, and googleapis.
For ad hoc execute, Kody scans literal bare import specifiers, synthesizes an
ephemeral dependency manifest, and resolves each package at its current registry
version during bundling. Relative and absolute imports and kody:,
cloudflare:, and node: specifiers are not npm dependencies. Registry
resolution failures and packages that cannot bundle for the Workers runtime are
reported as bundle errors. Saved packages continue to declare dependency
versions in their checked-in package.json. Ad hoc execute skips publish
checks, so a large import can succeed here and still fail package bundle
validation; see
Offload work that does not fit a Worker isolate.
For runtime details, see Cloudflare's Node.js compatibility.
Chaining
Prefer one execute when the plan is clear: import what you need, call several capabilities or package exports, branch on results, and return the final structured result. Split into multiple execute calls only when you need new user input, confirmation, or a result that changes the plan.
Plain execute has a hard timeout (~90s by default). Sandbox outbound fetch
is capped at 60s (30s under that budget) unless the caller passes a tighter
AbortSignal. For multi-step or long-running work (>~60s, batch sweeps,
migrations, polling loops), use one execute call to submit
workflows.create({ code, params }), then inspect progress with
workflowRunList instead of chaining many MCP tool calls. Sandbox timeout
errors state the enforced budget (for example
Execution timed out after 90s: …) and carry a structured next step pointing at
workflows.
Recovering from MCP client timeouts
A timeout means Kody stopped observing the sandbox. Kody cooperatively aborts nested package work where the execution model allows it, but already-started remote work and side effects may still complete. Do not blindly retry a timed-out side-effecting call: key-less successful execute calls are not written to Activity, so the timeout can leave you unsure whether the effect happened.
Pass an optional idempotencyKey (string, max 256 characters) when the call
must be recoverable:
- Kody persists that execute run eagerly (a
runningrow at start, terminal row at finish) and stores a bounded result snapshot on the run record. - The tool response includes
runIdwhenever a record exists — pollrunGetwith it if the client timed out. - Retrying with the same
idempotencyKeyafter completion returns the retained result withreplayed: trueand the samerunId(no second sandbox). - Retrying while the first attempt is still running returns
inProgress: truewith therunId(no duplicate start). - If a keyed run is stranded as
running(for example the Worker isolate reset before the terminal write), Kody reconciles it to an error witherrorName=platform_interruptedafter a few minutes (platform weather; outcome unknown — not a user-authored package failure). PollingrunGetor retrying the same key then returns that terminal outcome instead ofinProgressforever.
Omit the key for ordinary short calls; key-less execute stays on-failure-only so Activity is not flooded with successful one-offs.
To read field shapes while coding, use search with
entity: "capability:{name}" for builtin capability type definitions, or
inspect the relevant saved package with entity: "package:{package-name}".
Capability detail includes a complete execute module snippet; the runtime
call itself is always through the imported kody object.
Saved packages
Saved packages, package jobs, and one-off execute code share the same module-oriented runtime model:
- saved packages persist repo-backed source rooted at
package.json - package identity is the scoped
package.json.name(@scope/leaf) or the UUIDpackage_idwhen the name is not known; the leaf is the URL slug - package exports are defined by standard
package.json.exports - package-specific metadata lives under
package.json#kody - package jobs are schedules declared under
package.json#kody.jobs - package apps are optional UI surfaces declared under
package.json#kody.app - deferred one-shot work uses
workflows.create({ runAt, ... })fromexecuteor package runtime — see Workflows kody.jobUpdate(...)updates metadata on an existing job (schedule, timezone, enabled, kill switch, preserved,expires_at). Package job name and source stay in the package repo.- Optional
expires_atonjobUpdatestops the platform from scheduling the job after that UTC time. When expiry is reached, Kody auto-disables the job (enabled=false) so it shows as disabled injobList/jobGet(withexpired: true) and can age out via normal retention. This is separate frompreserved, which only skips auto-deletion. kody.jobGet({ id, includeCode: true })returns the scheduled job inspection details plus the stored entrypoint path and source code- Package-owned jobs are removed by editing the package and publishing, not by
jobDelete. kody.jobRunNow(...)runs an existing scheduled job immediately and returns both the updated job state and the execution result for debugging. Expired jobs are rejected.
Static saved-package imports from ad hoc execute run under the ad hoc
execute runtime. That means imported package modules can share exported helpers,
but packageContext remains null because the imported module has not been
entered as its own package runtime. Imported modules keep stamped
packageStorage() and stamp-aligned secret authority: A's export may use
secrets locked to A (or A's kody.secretMounts) without granting those secrets
to the execute entry. The execute entry cannot select A's id on
kody.packageSecretGet / Has; only A's stamped packageSecrets binding
carries that authority. Unstamped execute entry code can still use your user
secrets through {{secret:...}} placeholders; it cannot use A's mounts.
packageContext stays null on ad hoc execute.
When you need to edit saved source, prefer the repo-backed workflow in Repo-backed editing sessions. Open by package identity instead of internal source ids whenever possible.
For common edit-and-check workflows, use the file-level repo session API
documented in Repo-backed editing sessions: open a session
with repoOpenSession, edit with repoEditFiles or repoApplyPatch, inspect
with repoDiff, commit with repoCommit, validate with repoRunChecks, and
publish with repoPublishSession. There is no git-command channel inside
sessions; use packageGetGitRemote for full git.
Agent turns
Generic tool-using agent turns are package-owned behavior rather than a built-in runtime primitive. Search for an agent-turn package, then import that package from execute or another saved package.
Storage
Ad hoc execute has no scratch SQLite helper. Persist durable state from a saved
package with packageStorage(). Another package's data goes through a
static import of that package's export so its stamp does the reading and
writing. See Package storage.
packageStorage() exposes get / set / list / sql / delete / clear /
id. sql(...) returns a result object, not the row array directly:
import { packageStorage } from 'kody:runtime'
const result = await packageStorage().sql('select value from counters')
return {
columns: result.columns,
rows: result.rows,
rowCount: result.rowCount,
rowsRead: result.rowsRead,
rowsWritten: result.rowsWritten,
}
Read query rows from result.rows. The other fields are useful for
inspection and debugging, especially when validating whether a query read or
wrote storage.
For dedicated inspection, use:
storageExport— export one storage bucket as JSONstorageQuery— run SQL against one storage bucket (read-only by default, opt into writes explicitly)
Both are scoped to the caller. Code running as a saved package (invocations,
jobs, package apps, retrievers) may name only buckets that package owns — its
own bucket, its app facet buckets, and its package job buckets. Another
package's bucket is reachable only through packageStorage(), which is gated by
bundler-recorded provenance grants.
Long-term memory
Kody can surface a small number of relevant long-term memories when you pass a
short memoryContext on normal MCP tool calls. search also retrieves from
the query string. Surfaced memories appear in the tool text as subject and
summary only (id in structured content). Later retrievals can repeat that
compact block. Full contract (including dedupe_key collapse and the verify
workflow): Memory and conversation context.
Handled execute responses also include top-level timing metadata with
startedAt, endedAt, and durationMs alongside conversationId. Use it for
basic latency instrumentation around tool runs.
When a call is denied by a plan limit or a daily quota, the existing error
message and isError: true stay the same. Structured content also includes a
focused entitlement object with known fields only (resource, current plan,
limit, current usage, upgrade hint). Daily quota denials add compact used and
remaining. Unique-worker-day include denials add a short mechanic line
(meter name and what a unique worker day is). Ordinary successful execute
results omit entitlement.
Dynamic Worker identity for an execute run follows the acting user and that
module graph. The same user and graph reuse one isolate for the UTC day when
only params or packageContext change. The cost model is documented once in
Platform efficiency.
For memory mutations, the workflow is explicit and strict:
- Always run
metaMemoryVerifybefore writing or deleting memory - then decide whether to call
metaMemoryUpsert,metaMemoryDelete, both, or neither metaMemoryUpsertcreates a new memory whenmemory_idis omitted and updates an existing memory whenmemory_idis provided
Kody retrieves related memories, but the consuming agent is responsible for deciding what action to take.
MCP server instructions
Users can read or replace their own MCP server instruction overlay with
metaGetMcpServerInstructions and metaSetMcpServerInstructions.
This overlay is appended after Kody's built-in server instructions for that
user. Prefer memories for durable facts and preferences; use the overlay
only for rare always-on session policy — not for maintaining a package
inventory. When agents have used saved packages via MCP execute, Kody may
include a short “often used packages” hint automatically; discover others with
search. Pass an empty string to clear the overlay. Changes apply to new
MCP sessions, so reconnect the MCP client if the host caches server
instructions.
Some MCP clients keep only the first 2048 characters of server instructions.
metaGetMcpServerInstructions and metaSetMcpServerInstructions report
assembled_chars and a warning when the assembled text meets that cut, so the
overlay may never reach the model.
Network and OAuth helpers
The sandbox exposes global fetch plus secret placeholders in approved
contexts. OAuth and secret-header helpers are imported from kody:runtime:
import { createAuthenticatedFetch, oauthClientCredentials, secretHeaders } from 'kody:runtime'
createAuthenticatedFetch(providerName) is async. Await it before calling the
returned fetch wrapper:
const googleFetch = await createAuthenticatedFetch('google-business')
const response = await googleFetch('/calendar/v3/users/me/calendarList')
Integration names should usually follow <provider>-<purpose> when multiple
accounts may exist, such as google, google-business, or
google-youtube-brand. Call integrationList up front when a provider may
have multiple accounts connected.
OAuth integrations may include authorization metadata with the saved
authorizeUrl, requested scopes, any non-default scopeSeparator, and
provider-specific extraAuthorizeParams. Use that metadata when a refresh token
is stale and the user needs to reconnect; open
/connect/oauth?provider=<integration-name> instead of guessing the scope set.
For OAuth 2 client_credentials token exchanges that require
Authorization: Basic base64(client_id:client_secret), save the client id and
client secret separately. Do not ask the user to precompute or save the
derived Basic header. Use secretHeaders.basic(...) directly in a fetch header,
or use oauthClientCredentials(...) for the token request. These examples use a
placeholder API host and generic client credential secret names:
import { oauthClientCredentials } from 'kody:runtime'
export default async function main() {
const token = await oauthClientCredentials({
tokenUrl: 'https://api.example.com/oauth/token',
clientIdSecret: 'exampleClientId',
clientSecretSecret: 'exampleClientSecret',
scope: 'user',
})
return { tokenType: token.token_type, hasAccessToken: !!token.access_token }
}
For the lower-level token request built directly with secretHeaders.basic, see
the worked example in Secrets and host approval.
Kody resolves both saved secrets server-side, requires the token endpoint host to be approved for both secrets, and only sends the derived Basic header in the outbound request.
See Secrets and host approval for placeholders, host
approval, kody.secretList / secretSet, and the rules for mentioning
placeholder syntax without resolving it (the inert {{secret:<name>}} form and
the x-kody-secret-resolution: off header). Treat placeholder syntax as
operational wiring, not prose — never place a resolvable {{secret:...}}
token into content shown to users or sent to third parties.
Named state
Durable facts and preferences belong in memories. Package runtime state and
knobs belong in packageStorage(). Versioned config belongs in a repo. API
keys, PATs, and HMAC secrets belong in the secret store. OAuth access and
refresh tokens, and a user-registered app's client secret, live on the
integration.
Returning content blocks
By default, execute returns text output. To return non-text MCP content
blocks such as images, return an object with a __mcpContent array instead;
see Raw MCP content blocks.
The same passthrough applies when execute returns a result from a user-added MCP server that already includes protocol image (or other non-text) content blocks.
responseLimit caps ordinary JSON/text output (~100 KB by default).
Protocol __mcpContent blocks use a separate ~512 KB content cap so valid
images larger than 100 KB are not collapsed into truncated JSON.