Skip to content

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: [/* ... */],
});

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

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 extraction
context: (event) => event.targetBranch;
// Object literal with string operations
env: (event) => ({ BRANCH: event.targetBranch });
// Concatenation with event data
concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
// Using safe globals
env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
// Local variables are fine
context: (event) => {
const parts = event.targetBranch.split('/');
return parts[parts.length - 1];
};

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:

  1. The orchestrator dispatches a special __init__ job to a builder agent
  2. The builder agent clones the repository and evaluates the function
  3. The resolved values are sent back to the orchestrator
  4. 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 inlined
context: async (event) => await lookupEnv(event.targetBranch);
// External module references
env: (event) => {
const config = require('./config');
return config.env;
};
// Process/global access
context: (event) => process.env.DEFAULT_ENV || 'staging';
// Dynamic imports
env: async (event) => {
const m = await import('./config.js');
return m.default;
};
Evaluation pathOverheadWhen used
Static value (string/object literal)~0mscontext: 'staging'
Inline expression (pure function)~0mscontext: (event) => event.targetBranch
Init job (impure function)~5-10scontext: 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. Run kici preview to 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 on event.type ('push', 'pull_request', 'tag', …) to branch per trigger kind. The raw provider webhook body is nested at event.payload (for GitHub pushes: payload.ref, payload.after, payload.repository, …).