@softr/workflows-cli
v0.0.1
Published
Build, test and publish Softr Workflows from local files
Readme
@softr/workflows-cli
softr-workflows builds, tests and publishes Softr Workflows from
files on disk. It exists so that a coding agent (or a developer) can treat a workflow like a small project: the
workflow definition is a softr-workflows.jsonc file, every Run custom code node is a real .js or .py
file next to it, and the CLI moves both to and from the Softr workflows service. Nothing runs locally; every test
and every execution happens in the workflows service.
Status: proof of concept. Versions below 1.0 may change between releases; a single-binary build is planned.
Install and run
npm install -g @softr/workflows-cli # then: softr-workflows --help
npx @softr/workflows-cli --help # without installing
npm run dev -- --help # from a checkoutNode 22 or newer. npm update -g @softr/workflows-cli updates a global install; npx fetches the latest release on
its own.
Internal testers install from this repository before a version is on npm; see docs/internal-testing.md.
Agent skill
The agent's guide to this tool lives in skills/softr-workflows/SKILL.md, with the
custom-code sub-skill for writing the code of a node. The layout and
frontmatter follow softr-io/softr-as-code (skills/<name>/SKILL.md, sub-skills in subfolders). The skill tells the
agent how to install and update the CLI, so adding the skill is the only step a user takes.
The skills CLI installs it into the agent directories it knows, from this
repository (private: your git credentials are used) or later from a public one:
npx skills add softr-io/softr-workflows-cli --skill softr-workflows -a claude-code -g -y # user level
npx skills add softr-io/softr-workflows-cli --skill softr-workflows -a '*' -y # this project, every agentLinking the folder by hand works as well: mkdir -p ~/.claude/skills && ln -s <clone>/skills/softr-workflows
~/.claude/skills/softr-workflows. Other agents take the same folder wherever they read SKILL.md files. How the skill reaches users who do not have this
repository is still being decided.
Log in
The PoC authenticates with your studio session token. Copy the value of the auth cookie from your browser's dev tools (Application > Cookies on the studio site) and store it:
softr-workflows login --token <jwt>login checks that the value is an unexpired JWT, refuses personal API tokens (the workflows service does not accept
them from a CLI yet) and confirms the token with the studio before storing it, so you see who you are logged in as. SOFTR_WORKFLOWS_TOKEN overrides the stored token. Tokens live in
~/.softr/credentials.json, keyed by the API host.
The CLI targets Softr production. For development, SOFTR_WORKFLOWS_API_URL, SOFTR_WORKFLOWS_STUDIO_API_URL and
SOFTR_WORKFLOWS_STUDIO_URL point it at another workflows service, studio API and studio, for example locally running
ones. A workflow directory remembers the
target it was pulled from.
Project layout
One directory holds exactly one workflow. A parent directory may group several.
my-project/
enrich-leads/
softr-workflows.jsonc # the workflow (comments allowed)
actions/
enrich-record.js # body of the CUSTOM_CODE node "enrich-record"
score.py
.softr-workflows/ # CLI state: remote version/draft, cached sample outputs (gitignore it)
weekly-digest/
softr-workflows.jsoncsoftr-workflows.jsonc
The file has the shape of the workflows API write model (title, workspaceId, triggers, actions, paths,
configuration) plus the server id. Two CLI conventions apply:
- File references. Any string value of the form
"@@/relative/path"is replaced by that file's content onpush. The path is relative to the config file.pulluses it for everyCUSTOM_CODEnode'sinputs.code. - Readable node ids. Ids such as
enrich-recordare fine; the service only requires uniqueness and no:::. Placeholders then read{outputs.enrich-record:::$.body}. - Schema.
$schemapoints at.softr-workflows/schema.json, generated from the same catalog that drivesaddandspec: known node types get their inputs, enums and the path condition grammar; node types outside the catalog are accepted with free inputs, and drafts may hold nulls. The service remains the authority at publish.
Validation stays in the service: push saves a draft, publish (planned) runs the full validation.
Code files
The service stores the body of a function and runs it with a single variable, inputData, holding the node's
"Data to use in the code" map (inputs.inputData in the config). Locally the body lives inside a real function so
that editors, linters and agents see valid code:
export default function (inputData) {
return inputData.email.endsWith('@softr.io');
}def main(inputData):
return {"ok": inputData["email"].endswith("@softr.io")}pull writes a generated header above the function: the rules of the runtime and one typed line per inputData
entry. The types come from the saved test outputs of the referenced nodes (trusted), then from their declared output
metadata (marked as not seen in test data), and are unknown with a hint when the upstream node was never tested.
JavaScript gets a JSDoc @typedef with @property lines, so editors type-check inputData; Python gets the same as
comments. The header is regenerated on every pull and is never uploaded.
push uploads only that function body. Anything outside it (imports, helpers, constants) is rejected with a
message. JavaScript runs synchronously with no imports; Python may import the standard library inside main. The
return value becomes the node output at $.body. Never write workflow placeholders such as {outputs.x:::$.y}
inside code; map them in inputData instead. See the
Run Custom Code docs.
Commands
| Command | What it does |
| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| login --token <jwt> | Check, confirm and store your session token. |
| list [--workspace <id>] | List the workflows you can access, one table per workspace. A workspace whose listing fails is reported at the end and skipped. |
| pull [<workflowId>] [--dir <path>] [--force] | Download a workflow into a directory (new ./<title-slug>/ by default). Without an id, list what you can pull. |
| push [--dir <path>] [--dry-run] [--force] | Inline @@ references and code files, save as a new draft. Sends nothing when the files match the last sync, and refuses when the server copy changed since then (--force overrides both). Warns about placeholders that no saved test data backs. |
| init <title> [--workspace <id>] [--trigger <TYPE>] [--dir <path>] | Create a workflow on the server with one trigger (default WEBHOOK) and the local directory for it. |
| add <TYPE> [--id <id>] [--after <nodeId>] [--in-loop <loopId>] [--to <actionId>] [--lang JAVASCRIPT\|PYTHON] [--title <t>] | Append an action after a node (default: the only node without a successor) and the path to it, or append a trigger (--to connects it). Custom code gets actions/<id>.js\|py with an empty function. Comments in the config survive. |
| replace <nodeId> <TYPE> | Give a node another type of the same kind: id, title and paths stay, inputs start from the new template. |
| remove <nodeId> | Remove a node and the paths to and from it; a loop goes with its body. Code files stay on disk. |
| spec [<TYPE>] | The node types this CLI offers; with a type: inputs, output, notes, an example node and the docs URL. |
| schema [--write] | Print the JSON schema of softr-workflows.jsonc, generated from the node catalog, or write it into the workflow directory. pull and init write it and set $schema in the config, so editors validate and complete the file. |
| status | Local edits vs the last sync, the server's version and draft, the live version. |
| publish [--force] / unpublish | Enable the latest version (the service validates the whole workflow) or disable the workflow. publish fetches the server copy first and refuses unpushed local edits and a server copy that changed since the last sync; then it lists nodes that are untested, failed, tested before the last save, or tested against outdated upstream data: a terminal asks for confirmation, a script or agent needs --force. |
| test <nodeId> [--mock] [--iteration <n>] [--sample <n>] | Run one node in the workflows service and save its sample output. Triggers propose samples; --sample picks one (default the first). |
| test --all | Run the trigger and then every action in execution order, predecessors first across parallel paths and loop bodies. Branches, filters and loops are skipped. Failures are reported at the end; the exit code is 1 when any test failed. |
| outputs | One line per node: tested, failed, untested, with the field count. |
| outputs <nodeId> | The node's referenceable variables with types and examples, ready to paste. |
| outputs --refresh | Fetch the saved samples from the service and regenerate the code headers. |
| verify [--test-missing] | One readiness report: code files parse, local files match the server, the service accepts the definition for publishing, every node tested on current data, every placeholder backed by test data. --test-missing first runs the tests that are missing, in execution order. Exit code 1 while something needs attention. |
| placeholder <nodeId> [keys...] | Build the token for a node's output field from keys (* for every element, digits for an index, or one dotted path), checked against the saved test data: type and example when it exists, the keys that do exist when it does not. A loop id gives the current item. |
Every command accepts --json. After a pull, the target is remembered in .softr-workflows/state.json.
Every test and outputs --refresh updates .softr-workflows/outputs.json and rewrites the code headers, so the
types of inputData follow the latest test data. Tests run against the server's copy of the workflow. test compares
the local files with that copy and names the nodes whose edits are not pushed yet, so a green result is never mistaken
for a verdict on local code.
verify, status and the publish guard read the service's readiness report
(GET /v1/workflows/{id}/readiness): the publish validation as a dry run with every message, and per node the test
state, staleness against the last save, outdated references and placeholders no saved sample backs. On a deployment
without that endpoint the CLI runs the same node checks itself and says so; the validation dry run is then missing.
.softr-workflows/state.json keeps the server version and draft of the last sync and a content hash of what was
synced. That is how push knows there is nothing to send, how it notices that the server copy moved on, and how
test tells edited from unedited without any server-side support.
The node types on offer
spec lists them. The set is small on purpose: the generic triggers (webhook, schedules, Softr Tables and Softr Apps
events, incoming email), the control nodes (branch, filter, loop, wait), RESPONDED_TO_WEBHOOK, CALL_API, and
CUSTOM_CODE for everything else. Other integrations stay in the studio. Each entry points to its public docs page.
A typical session
softr-workflows init "Lead scoring" # server + ./lead-scoring/
cd lead-scoring
softr-workflows add CUSTOM_CODE --id score --lang python # actions/score.py + node + path
# edit actions/score.py, set inputs.inputData in softr-workflows.jsonc
softr-workflows push
softr-workflows test --all # trigger first (send it one request), then score
softr-workflows outputs score # variables the next node can use
softr-workflows publishDevelopment
npm ci
npm run check # typecheck, prettier, tests, build
node dist/cli.mjs --helpThe build is one self-contained ESM file (dist/cli.mjs), which keeps a later bun build --compile possible: no
native addons, no runtime reads relative to __dirname.
CI (.github/workflows/ci.yml) runs npm run check on every pull request and every push to master.
Internal testing
docs/internal-testing.md is the tester's guide: install the CLI from this repository before a version is on npm, link the skill, and point the CLI at a staging environment.
Releasing
A release is a version bump. Merging a change to version in package.json to master publishes that version to
the public npm registry (.github/workflows/publish.yml, the pattern of softr-io/softr-as-code); every other change
publishes nothing. The workflow runs npm run check first, then npm publish.
- Versions stay below 1.0 while the tool is a proof of concept; they are published on the
latesttag. - Authentication is npm trusted publishing over OIDC, registered on npmjs.com for this repository and the workflow
file name
publish.yml. Renaming the file or moving the publish step breaks publishing until it is re-registered. The first version of a package must exist before trusted publishing can be configured, so a maintainer of the@softrnpm organization publishes0.0.1by hand. - Releases are not tagged, and provenance is off, both deliberately (the repository is private).
- Re-running the workflow by hand is not a retry: once
mastermoves on, the previous commit no longer holds the pre-bump version. Use "Re-run failed jobs" on the original run instead.
Further reading
- Workflows docs index for agents: https://docs.softr.io/llms.txt (every page is also served as
.md). - Triggers: https://docs.softr.io/workflows/trigger-types.md
- Advanced concepts (Branch, Loop, Run Custom Code, Call API): https://docs.softr.io/workflows/advanced-concepts.md
