Dynamic values
Dynamic values let you compute context, env, and concurrencyGroup at runtime based on the incoming event. Instead of hardcoding static strings, you pass a function that receives the normalized event envelope and returns the resolved value.
job('deploy', { runsOn: ['default'], context: (event) => event.targetBranch, env: (event) => ({ BRANCH: event.targetBranch }), concurrencyGroup: (event) => `deploy-${event.targetBranch}`, steps: [/* ... */],});job('deploy', { runsOn: 'default', // One shape everywhere: branch on the normalized event type. context: (event) => (event.type === 'pull_request' ? 'preview' : 'production'), steps: [/* ... */],});How it works
Section titled “How it works”When you define a dynamic value as a function, the compiler analyzes it at compile time to determine whether it is pure (can be evaluated without cloning the repo or running an init job).
Pure functions (inline evaluation)
Section titled “Pure functions (inline evaluation)”A pure function is one that:
- Is synchronous (no
async/await) - Only references its parameters and local variables
- Does not import or require external modules
- Does not access globals like
process,fetch,console,setTimeout, etc. - Uses only safe built-in constructors:
String,Number,Boolean,Array,Object,JSON,Math,parseInt,parseFloat,isNaN,isFinite,encodeURIComponent,decodeURIComponent,encodeURI,decodeURI - Does not use
this,new,class,throw,try/catch,delete,var,yield, or mutation operators (++,--,+=, etc.)
When the compiler detects a pure function, it serializes the function source directly into the lock file as an inline expression. At dispatch time, the orchestrator evaluates the expression in a sandboxed VM context (~0ms overhead) instead of dispatching an init job.
Examples of pure functions:
// Simple branch extractioncontext: (event) => event.targetBranch;
// Object literal with string operationsenv: (event) => ({ BRANCH: event.targetBranch });
// Concatenation with event dataconcurrencyGroup: (event) => `deploy-${event.targetBranch}`;
// Using safe globalsenv: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
// Local variables are finecontext: (event) => { const parts = event.targetBranch.split('/'); return parts[parts.length - 1];};Impure functions (init-job evaluation)
Section titled “Impure functions (init-job evaluation)”If the compiler determines a function is impure, it prints a warning [W101] naming the affected field (context, env, or concurrencyGroup), the reason the function was judged impure, and the ~5-10 second init-job cost — and compilation still succeeds. The function falls back to the two-phase init model. This means:
- The orchestrator dispatches a special
__init__job to a builder agent - The builder agent clones the repository and evaluates the function
- The resolved values are sent back to the orchestrator
- The orchestrator dispatches the real execution job with the resolved values
This adds approximately 5-10 seconds of overhead for cloning and evaluation.
kici preview lists the injected __init__ job under each affected job, so you can spot the init-job cost before the first run.
Examples of impure functions (will use init job):
// Async functions cannot be inlinedcontext: async (event) => await lookupEnv(event.targetBranch);
// External module referencesenv: (event) => { const config = require('./config'); return config.env;};
// Process/global accesscontext: (event) => process.env.DEFAULT_ENV || 'staging';
// Dynamic importsenv: async (event) => { const m = await import('./config.js'); return m.default;};Performance comparison
Section titled “Performance comparison”| Evaluation path | Overhead | When used |
|---|---|---|
| Static value (string/object literal) | ~0ms | context: 'staging' |
| Inline expression (pure function) | ~0ms | context: (event) => event.targetBranch |
| Init job (impure function) | ~5-10s | context: async (event) => await lookupEnv(event.targetBranch) |
- Write pure functions whenever possible to avoid the init-job delay. Most context and env computations only need the event payload data.
- Check compiler warnings — the compiler prints a
warning [W101]when a function is classified as impure, naming the reason and the ~5-10s init-job cost. Runkici previewto see the injected__init__job listed under each affected job before your first run. - Runtime errors in inline expressions cause immediate job failure. There is no fallback to the init-job path. If your pure function throws at runtime (e.g., accessing a property on
undefined), the job fails immediately. - See how your workflow code executes for the full picture of where pure vs. impure functions run relative to rules, hooks, and step bodies.
- The event parameter is the normalized event envelope — the same shape rules receive as
ctx.event:{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }(see the event payload reference for the complete schema). Narrow onevent.type('push','pull_request','tag', …) to branch per trigger kind. The raw provider webhook body is nested atevent.payload(for GitHub pushes:payload.ref,payload.after,payload.repository, …).