CLI reference
The shiftlabs command deploys workflows and agents from your machine or CI, and follows their runs. It needs Node 22.18 or later and an API key.
Install
Install the shiftlabs package in your project, or run it with npx. The package also gives import { workflow } from "shiftlabs" its types.
npm install --save-dev shiftlabs
export SHIFTLABS_API_KEY=slk_… # developer: deploy and run; viewer: read only
npx shiftlabs whoami
- Node 22.18 or later. Bun runs it too.
SHIFTLABS_API_KEYis an API key.initneeds none. The CLI reads the key only from the environment.--api-url, orSHIFTLABS_API_URL, sets another API origin. The default ishttps://api.shiftlabs.org.- Every command takes
--jsonto print JSON. A failure prints a JSON error instead. See Errors. - The CLI never asks a question or waits for input, so it runs the same in CI and from an AI coding tool.
Commands
shiftlabs --help lists the same commands and options.
| Command | What it does |
|---|---|
init | Lists the agent templates. Writes nothing. |
init <dir> | Writes AGENTS.md and CLAUDE.md for coding agents into dir (. for the current directory), and lists the agent templates. See Coding agents. |
init --template <name> [dir] | Writes a template's shiftlabs.config.ts and workflows into dir, by default the current directory, with AGENTS.md and CLAUDE.md. See Templates. |
deploy | Deploys every workflow and agent in shiftlabs.config.ts as one release. See Projects. |
deploy <file>... | Deploys workflows from .ts, .js or .py files, each current at once, with the files and packages they import. The file name is the workflow name. |
releases | Lists recent releases, newest first. |
rollback <release> | Makes an earlier release's versions and schedules current again, as a new release. |
run <workflow> | Starts a run of the current version now. |
runs | Lists recent runs, newest first. |
logs <run-id> | Prints a run's log lines. |
whoami | Shows the workspace, the key's role and the plan. |
types | Writes the workspace's secret and var names, never values, its agent names and each workflow's payload type as editor types. See Editor types. |
| Option | For |
|---|---|
--config <file> | deploy: the project file. Default shiftlabs.config.ts, then .mts, .js and .mjs. |
--out <dir> | types: where to write. Default: the folder of shiftlabs.config.ts, else the current directory. |
--template <name> | init: the template to start from. |
--name <workflow> | deploy: the workflow name, for a single file. |
--payload <json> | run: the run's JSON payload. Default {}. |
--workflow <name> | runs: only this workflow. |
--status <status> | runs: queued, running, sleeping, completed or failed. |
--limit <n> | runs, logs, releases: how many. Default 20 runs, 500 lines, 20 releases. |
--json | Prints JSON instead of text, errors included. |
--api-url <url> | Default $SHIFTLABS_API_URL, else https://api.shiftlabs.org. |
-h, --help · -v, --version | Help and the version. |
deploy <file>deploys.tsand.jsfiles as TypeScript and.pyfiles as Python. Underscores in a file name become hyphens:nightly_report.pydeploys asnightly-report.- A deploy with new Python packages or npm packages waits for them to build, up to 10 minutes.
deploysends the files a workflow imports with it. See Files.runsends oneIdempotency-Key, and on a network error, a 30-second timeout or a5xxtries again with it, 3 attempts at most. A retry never starts a second run.- Developer keys deploy, release, roll back and run. Viewer keys read, and can run
types.
Errors
With --json, a failed command prints one JSON object on stdout. Without it, the message goes to stderr. The exit code is the same either way, and tells the kind of failure.
{
"error": {
"code": "limit",
"message": "POST /v1/workflows/refund-check/runs -> 429: quota exceeded",
"hint": "Try again in 3600 seconds.",
"status": 429,
"response": { "error": "quota exceeded", "limit": "runsPerDay", "max": 1000 }
}
}
codeandmessageare always there.hintsays what to do next, when there is something to say.statusandresponseare there when the API answered: its HTTP status and its JSON body.
| Code | Exit | Means |
|---|---|---|
usage | 2 | A command, option or argument is wrong. |
config | 3 | Something on your side needs fixing: the project file, a workflow file, the API URL, or a CLI too old for the API. |
auth | 4 | SHIFTLABS_API_KEY is not set, or the API did not accept it. |
forbidden | 5 | The key's role or scope does not allow this, or the workspace is suspended. |
not_found | 6 | The workflow, run, release or other item does not exist. |
conflict | 7 | Something is in the way. init does not overwrite files. |
invalid | 8 | The API refused the request, such as code that does not build. |
limit | 9 | A plan limit or a rate limit. The hint says when to try again, if the API said. |
network | 10 | The API could not be reached, or did not answer in time. |
server | 11 | The API failed. Try again shortly. |
internal | 1 | A bug in the CLI. |
Projects
shiftlabs deploy with no files reads shiftlabs.config.ts. It lists the project's workflows, agents and schedules, and deploys them as one release.
import { agent, file, project, schedule, workflowTool } from "shiftlabs";
export default project({
workflows: {
"lookup-invoice": "./workflows/lookup-invoice.ts",
"nightly-report": "./workflows/nightly_report.py",
},
agents: {
billing: agent({
title: "Billing assistant",
instructions: file("./agents/billing.md"),
model: "@cf/zai-org/glm-4.7-flash",
tools: [workflowTool("lookup-invoice", { description: "Look up an invoice by its id." })],
}),
},
schedules: {
nightly: schedule("nightly-report", { cron: "0 3 * * *", timezone: "America/Detroit" }),
"monday-summary": schedule({ agent: "billing", message: "Summarize last week's refunds.", cron: "0 9 * * 1" }),
},
});
- Stage
Each workflow is deployed with
?stage=1: built and stored, not yet current. - Upload code
Each agent with
codeuploads that file. Every code tool in it joins the agent. - Release
One
POST /v1/releasesmakes the workflow versions, agent definitions and schedules current together. It prints the release number and what changed.
- If a step fails, nothing that runs changes. Staged versions and uploads that no release names are dropped after a day.
- With nothing changed,
deployprintsNothing to release: every workflow, agent and schedule is already current.and records no release. CI can deploy on every push. - Paths are relative to the config file. The config must
export default project({...}). If it importsshiftlabs, install the package first. - An agent's
codeis the path of its tools file, whose default export istools({...}). The agent'stoolstakesworkflowTool(...)only. See Code tools. - An agent's
instructions: file("./x.md")sends the file's text, up to 8,000 characters. - A Standard JSON Schema in a workflow tool's
parameters, such as a zod 4.2+ schema, is sent as its JSON Schema. - A key that deploys agents needs the
agent.deploycapability, which developers hold. schedulesreplaces every schedule in the workspace, including ones edited in the dashboard.schedules: {}removes them all; a project withoutschedulesleaves them as they are. See Schedules.
Files and packages
A workflow's file can import other files and, in TypeScript, npm packages. deploy sends them with it, as one deploy of several files. A workflow that imports nothing of its own is sent as its text, as before.
- TypeScript:
deployfollows eachimport,export … from,import()andrequire()with a relative path, such as./money.ts,../lib/moneyor./rates.json, through the files they import. It sends.ts,.mts,.js,.mjsand.jsonfiles. - The files must sit in the folder of the nearest
package.json, looking up from the workflow's folder, or below it. Without apackage.json, they sit in the workflow's folder or below it. - When the code imports a package,
deploysends thatpackage.jsonand the part of itspackage-lock.jsonthe imports reach. The platform installs the packages the workflow imports at the versions the lockfile pins, anddeploywaits while a new set installs. Node.js built-ins such asbuffer, and imports of types only, need neither file. See Files and npm packages. - Without a
package-lock.json,deploywarns and sends the files withoutpackage.json, so no package installs, and the deploy fails if the code needs one. With apnpm-lock.yaml,bun.lockoryarn.lock, runnpm install --package-lock-onlyto write apackage-lock.jsonbeside it. - An import of
./money.jsthat meansmoney.tsstops the deploy. Import it as./money.tsor./money. - Python:
deploysends the modules the workflow imports from its folder and below, such asimport helpersorfrom lib.util import double.
Editor types
shiftlabs types reads the workspace's secrets, vars, agents and workflows through the API and writes their names, never values, and each workflow's payload type to two files next to shiftlabs.config.ts. Your editor then completes secrets. and vars., and flags a misspelled name before you deploy. In an app, shiftlabs/client type-checks agent names and payloads with the same file. Run it again after adding or removing a secret or var, or deploying an agent or a workflow.
npx shiftlabs types
# Wrote shiftlabs-env.d.ts and shiftlabs_types.pyi: 1 secret, 2 vars, 1 agent and 3 workflows.
npx tsc --noEmit
# workflows/refund.ts(4,30): error TS2551: Property 'STRIPE_KYE' does not exist on type 'Readonly<Secrets>'. Did you mean 'STRIPE_KEY'?
shiftlabs-env.d.tsTypeScript. It fills the
SecretsandVarsinterfaces of"shiftlabs", andAgentNamesandWorkflowPayloadsof"shiftlabs/client". Yourtsconfig.jsonmust include it; the defaultincludecovers the whole folder.shiftlabs_types.pyiPython. It takes effect with the
shiftlabsPython package. The package readsSecretsandVarsfrom this file. Each name is an attribute, andctx.secrets["NAME"]takes the names too. Annotate the context, as inasync def refund_check(ctx: Context), so the editor knows whatctxis. The file also lists the agent and workflow names asAgentNameandWorkflowName, whichshiftlabs.clienttakes.
secretslists only secrets with hosts, since only they have a stand-in. The command names the others.reveal()andsign()take any secret's name.- A var set only for some workflows is optional:
string | undefinedin TypeScript,str | Nonein Python. - In TypeScript, until the workspace has a secret with hosts, any
secrets.NAMEtype-checks, and the same for vars. In Python, the file for an empty workspace has no names, so the editor flags every one. - A workflow's payload type comes from its payload schema. A workflow without one takes any payload. The built-in helper isn't among the agents.
--out <dir>writes to another folder, such as an app's that uses the client.typesoverwrites only files it wrote.- Each part needs a capability: secrets
secret.view, varsvar.view, workflow names and payload typesworkflow.view, agent namesagent.view. A viewer key has all four. Withoutworkflow.vieworagent.view, that part is left empty, so any name type-checks there, andtypessays which capability the key lacks. - With
--jsonit prints{"files", "secrets", "vars", "workflowVars", "withoutHosts", "agents", "workflows", "lacking"}.lackinglists the capabilities the key missed.
Versions
The shiftlabs package has the same version as the SDK the platform runs, so its types match how your code runs. The CLI and the API check each other's version.
- Each request names the CLI's version in its
User-Agent, such asshiftlabs-cli/0.3.0. - When the API runs a newer version, the command prints one line to stderr with that version and the command to update. It still runs.
- The API refuses a CLI older than the oldest version it accepts. The command fails with
426 {"error": "cli too old"}, and the answer'shintsays what to install. See Versioning. - A release made with
shiftlabs deployorshiftlabs rollbackrecords the CLI's version ascliVersion. The dashboard's Releases screen shows it. - The package's
CHANGELOG.mdlists what each version changed. Before 1.0, a version can rename or reshape what earlier ones had.
Coding agents
shiftlabs init --template <name> and shiftlabs init <dir> write an AGENTS.md that coding agents such as Codex, Cursor and Claude Code read before they change the project. For a project you already have, run npx shiftlabs init . in it. The file covers the project's layout, the commands, and the rules that most often trip people up: code outside a step runs again, secrets are stand-ins, code tools have no network, and a deploy is one release. It links to these docs and their llms.txt. CLAUDE.md holds one line, @AGENTS.md, so Claude Code reads the same file.
initnever overwrites a file. IfAGENTS.mdorCLAUDE.mdexists, it leaves it as it was and says so. Add the line@AGENTS.mdto your ownCLAUDE.md.- If the template's
shiftlabs.config.tsor a workflow file exists,init --templatewrites nothing.