Jobs & Caching
Extensions expose functionality through commands (the user-facing API) and implement it with tasks (atomic executable units). The CLI orchestrates tasks across projects and uses caching to avoid redundant work.
Commands and tasks
A command is a user-facing operation (e.g., putnami build). Each command defines a pipeline of steps that reference tasks:
{
"commands": {
"build": {
"run": [
{ "id": "generate", "task": "build-generate", "dependsOn": ["^generate"] },
{ "id": "transpile", "task": "build-transpile", "dependsOn": ["generate"] }
]
}
},
"tasks": {
"build-generate": {
"kind": "command",
"command": "{extensionRoot}/bin/build",
"args": ["generate"],
"cwd": "{projectRoot}",
"timeoutMs": 300000,
"inputs": {
"source": { "from": "project", "files": ["src/**/*.ts"] },
"config": { "from": "project", "files": ["package.json"] }
},
"cache": { "enabled": true, "deterministic": true }
}
}
}Command fields:
| Field | Required | Description |
|---|---|---|
run |
yes | Pipeline step DAG |
description |
no | Human-readable description |
activationFiles |
no | File patterns that must exist for the command to activate |
flags |
no | CLI flags the command accepts |
priority |
no | Higher priority wins when multiple extensions provide the same command |
quiet |
no | Suppress from job recap |
channel |
no | Execution channel for concurrency control |
Task fields:
| Field | Required | Description |
|---|---|---|
kind |
yes | Execution kind (currently only command) |
command |
yes | Command to execute |
args |
no | Command arguments |
cwd |
no | Working directory (supports template variables) |
env |
no | Environment variables |
timeoutMs |
no | Execution timeout (default: 300000ms) |
inputs |
no | Input port declarations for cache invalidation |
outputs |
no | Output port declarations |
cache |
no | Cache policy ({ "enabled": true }) |
Template variables for command, args, cwd, and env:
| Variable | Description |
|---|---|
{workspaceRoot} |
Absolute path to workspace root |
{projectRoot} |
Absolute path to project root |
{extensionRoot} |
Absolute path to extension package |
Pipeline steps
Each step in a command's run array references a task and declares its position in the DAG:
Step fields
| Field | Required | Description |
|---|---|---|
id |
yes | Unique step identifier |
task |
yes | Name of a task in the extension's tasks section |
dependsOn |
no | Step IDs or external references this step depends on |
with |
no | Input bindings passed to the task |
if |
no | Plan-time condition (excludes step from DAG when false) |
when |
no | Runtime condition (skips step when false) |
optional |
no | If true, failure marks step as skipped instead of failed |
Dependency references
The dependsOn field supports several reference types:
| Pattern | Meaning |
|---|---|
"transpile" |
Wait for step transpile in the same command |
"^generate" |
Wait for the upstream dependency's generate step |
"*job" |
Wait for all downstream dependents |
"/project:build" |
Wait for another project's build command |
"@ext/name:job" |
Extension-scoped reference |
The ^ prefix is the most common — it creates cross-project dependency chains so downstream projects wait for their dependencies to complete the same phase.
Embedded flags
Append flags after a dependency reference to request a specific subset of work:
"dependsOn": ["^build --generate"]The planner splits each string on whitespace — the first token is the reference, the rest are forwarded as arguments. When multiple steps schedule the same dependency with different flags, the planner upgrades to an unrestricted execution so the result satisfies all dependents.
Task-level upstream dependencies
When a ^ ref matches a step in the current pipeline, the planner emits only that individual task for each upstream project — not the full pipeline:
{
"commands": {
"package": {
"run": [
{ "id": "generate", "task": "build-generate", "dependsOn": ["^generate"] },
{ "id": "transpile", "task": "build-transpile", "dependsOn": ["generate", "^transpile"] },
{ "id": "docker", "task": "package-docker", "dependsOn": ["transpile"], "if": "params.docker" }
]
}
}
}Running putnami package my-app (where my-app depends on sdk which depends on utils):
- utils and sdk get only
generateandtranspiletasks - my-app gets the full pipeline (generate, transpile, docker)
- No upstream docker tasks are scheduled
When the ^ ref does NOT match any step in the pipeline, the planner schedules the full command on upstream projects.
Input bindings
Steps can pass data to tasks via with:
{
"id": "transpile",
"task": "build-transpile",
"with": {
"target": { "value": "production" },
"config": { "from": "command", "path": "flags.target" },
"schemas": { "fromStep": "generate", "output": "schemas" }
}
}Conditional execution
if— evaluated at plan-time. Removes the step entirely from the DAG:"if": "params.compile"when— evaluated at runtime. Skips the step but keeps it in the DAG:"when": "steps.test.status == 'success'"
Orchestration
Job orchestration happens in two phases:
Planning:
- Resolve target projects (
--all,--impacted, explicit names, etc.) - Load extension command and task definitions
- Expand
dependsOninto concrete task nodes (project + task pairs) - Build a dependency DAG
Execution:
- Start all tasks concurrently
- Each task waits on its dependencies before running
- Results and events are collected into a single session
Parallelism is automatic — tasks without mutual dependencies run at the same time. Use --max-parallel <n> to cap concurrency.
If a dependency is already in the scheduling stack, the edge is skipped to avoid cycles. Planning continues, but the cycle indicates a configuration problem.
Use --plan to preview the execution tree without running:
putnami build --impacted --planCaching
Caching avoids re-executing tasks whose inputs have not changed.
How it works:
- Tasks declare
inputswith file globs (from: "project",files: [...]) - The runner computes a hash from matched files
- Cache keys combine:
- Task identity (extension, task name, project)
- Task parameters
- Dependent task hashes
- Project file hash
If any file matched by the input declaration changes, the hash changes and the cache is invalidated.
Flags:
--no-cache— Skip cache reads but still write resultsputnami cache clean— Delete all cached data
Watch mode
The --watch flag enables pipeline-aware file watching. When files change, running tasks are aborted and the pipeline is re-executed. Watch mode forces --no-cache to ensure fresh results on every iteration.
putnami test . --watch
putnami build --all --watch
putnami serve my-app --watchBehavior:
- Polling-based file system monitoring (100ms intervals)
- 150ms debounce for rapid edits
- Automatically excluded:
.git/,node_modules/,.putnami/,dist/,build/,__pycache__/,.venv/ - Changed files are mapped to projects, and only affected tasks re-run
- Dependencies propagate transitively — if a library changes, dependent apps re-run
Multi-service sessions
When running putnami serve, watch mode discovers and starts all runtime service dependencies alongside the primary target. Services are found via putnami.runsWith in package.json or by walking the dependency graph for projects with a ./serve export.
putnami serve web-app # Starts web-app + discovered services
putnami serve web-app --no-services # Only the target projectOn file change, only affected services are selectively restarted. Unaffected services keep running.
Disabling jobs
Jobs can be disabled at workspace or project level. Disabled jobs are excluded during planning.
Workspace level — in the root package.json:
{
"putnami": {
"disable": {
"jobs": ["lint"]
}
}
}Project level — in the project's package.json:
{
"putnami": {
"disable": {
"jobs": ["@putnami/typescript:lint"]
}
}
}Unqualified names (e.g., "lint") disable all jobs with that name. Qualified names (e.g., "@putnami/typescript:lint") disable only that extension's job.
Overriding extension jobs
A project can override extension-provided jobs in its putnami.json under the jobs key:
{
"name": "my-custom-app",
"extensions": ["@putnami/typescript"],
"jobs": {
"build": {
"kind": "command",
"command": "bun",
"args": ["run", "{projectRoot}/scripts/build.ts"],
"cwd": "{projectRoot}",
"dependsOn": ["^build"]
}
}
}Overrides only affect the specific project. The project acts as its own job provider — {extensionRoot} resolves to {projectRoot}. You can also define entirely new job names that don't exist in any extension.
Job status
Jobs return one of: OK, FAILED, or SKIP (and may be shown as cached when served from cache).
Common skip cases:
- A dependency failed
--skip-ciis set andCI=true- The job decides to no-op in its own implementation
- The job is listed in
disable.jobs
Command naming scheme
Putnami uses a consistent naming scheme across all extensions:
| Command | Scope | Description |
|---|---|---|
build |
project | Compile and bundle |
test |
project | Run tests |
lint |
project | Format and lint code |
serve |
project | Run development server |
package |
project | Create distribution packages |
publish |
project | Publish to registries and asset storage |
release |
workspace | Changelog, git tag, version bump |