Factorial Code — CLI
The fcode CLI develops and tests processes locally and syncs them with
Factorial Code Cloud. For the platform model see fcode-core-concepts.
Command flow
First time on a machine: install with pnpm install -g @factorialco/fcode-cli,
then fcode clone <workspace-slug> to bring a cloud workspace down into a
<workspace-slug>/ folder (it also installs these agent skills; skip that with
--skipSkillsSetup). The "How to build locally" guide on an App's Development
tab shows the exact clone command for its dev workspace — copy it from there,
the slug is an encoded token, not the App's UUID. To bring down every App
your development team owns in one layout, use fcode team:clone instead (below).
When making and testing changes in an already-cloned workspace:
fcode pull(optional, first) — if the cloud may have changed, sync down so you work with the latest version.- Edit local files (processes, modules, variables, dependencies).
fcode add— only when you created NEW resources (new process, module, dependency, or variable). Skip for edits to existing code.fcode dependencies:install— if you changeddependencies/package.jsonordependencies/requirements.txt.fcode run <process-slug>— execute the process locally to test.fcode push— deploy to cloud when ready.
Pushing updates the current (unversioned) code only: consumers pinned to the
stable alias — which webhook URLs and form embeds should always be — keep
running the released version until the alias moves. Releases (publishing a
workspace version and re-pointing stable) happen separately, normally from the
web UI (see below).
Gotchas
- Never run
fcode runbeforefcode addwhen the process (or other resource) was just created — you'll hit "Local process not found". fcode addis only for NEW resources. For edits to existing process/module code or variables, go straight tofcode push.--force(onpush/pull) overwrites the other side. Both commands fail when local and cloud diverge —settings:pullincluded, which refuses to wipe an unpushedsettings.json. Runfcode difffirst to show the user exactly what would be discarded, and only use--forcewith explicit user confirmation.- Editing
versions/aliasesinsettings.jsonreleases on push: adding a tag publishes that workspace version, removing one deletes it from the cloud (cascading), re-pointing an alias is a release or rollback. Same rule assettings:versions:*/settings:aliases:*— don't touch unless explicitly asked. - Never edit inherited resources.
variables.inherited.env,i18n/<locale>.inherited.yaml, and inherited processes/modules are owned by parent workspaces, regenerated on pull, and never pushed (see below). Override a variable by adding the key tovariables.env(a locale key toi18n/<locale>.yaml); edit processes and modules in the owning workspace.
Commands
fcode add
Registers new local resources with the CLI so they can be run or deployed.
Run after creating a new process/module/dependency/variable, before run/push.
Not needed after only editing existing resources.
fcode dependencies:install
Installs dependencies into the local workspace. Run after changing
dependencies/package.json or dependencies/requirements.txt. Installs the
inherited packages too, so a local run resolves what the cloud does.
fcode run <process-slug> --parameters <filepath | json>
Executes a process locally for development/testing. Uses variables.env /
variables.local.env and the given parameters (or the process's
parameters.json by default). Shows logs, results, and errors.
fcode run my-process --parameters '{"key": "value"}'
fcode run my-process --parameters ./params.json
fcode run my-process # default parameters.json
fcode run my-process --locale pt-BR # resolve fcode.i18n in that locale
Prerequisite: run fcode add first if the resource was just created.
--locale resolves fcode.i18n against the local i18n/ files exactly as the
cloud does — see fcode-i18n (including why a typo in the flag silently
resolves every key to itself).
fcode http
Starts a local HTTP server (--port, default 3000) that replicates the cloud
webhook environment and also serves the workspace's form schemas, so
webhook-triggered processes and forms can be exercised without deploying.
--auth-user / --auth-password protect the whole local server with basic
auth. Per-process webhook auth is separate, and enforced exactly as the cloud
does it: the server reads webhook.authMode from the process's metadata.json,
resolves TEAM against webhookAuth in settings.json, and requires the named
variable's value in the configured header — Bearer <token> in Authorization,
the raw value in any other header. Values come from the workspace variables in
the precedence every local run uses — variables.inherited.env, overridden by
variables.env, overridden by variables.local.env (below). Header lookup is
case-insensitive, but a repeated header is rejected rather than joined. A missing
header, a malformed bearer value, an undefined variable, a mismatch, or a
TEAM webhook whose workspace configuration is absent all get a 403, with the
reason printed to the console.
Two things to watch locally:
- A secret variable pulls down as the
********placeholder, so local auth only accepts********until the real value is invariables.local.env. --auth-user/--auth-passwordconsume theAuthorizationheader, so they can't be combined with a webhook expecting its credential there. The server warns about this at startup.
fcode pull
Downloads the latest processes, modules, variables, and dependencies from the
cloud, overwriting local files to match. Run before starting work if others may
have changed cloud resources, or to discard local changes. --force only with
user confirmation.
fcode push
Uploads local changes to the cloud. Run after local changes (run fcode add
first only if you created new resources); recommended to fcode run first.
--force only with user confirmation.
fcode status
Reports what differs between local and cloud without changing either —
processes, modules, variables, dependencies, locales, and settings.json.
Per-resource variants exist too: processes:status, modules:status,
variables:status, i18n:status, settings:status.
Inherited resources the cloud agrees on are hidden, since there is nothing
to pull or push for them; a line reports how many were left out. An inherited
resource reappears as soon as it differs — out-of-date, new, removed, conflict,
modified, sensitivity-changed — so a parent moving one is never silently
swallowed. --showInherited lists them all:
fcode status --showInherited # or any per-resource variant above
An inherited sensitive variable counts as unchanged and is hidden too: its value is never compared, so it could never report as up to date.
fcode diff [<slug>]
Shows what differs, where status only says that something does: a
git-style unified diff of every file the workspace is stored as, with the cloud
as the old side and the local files as the new one. Per-resource variants take
an optional slug: processes:diff, modules:diff, variables:diff,
i18n:diff, dependencies:diff, settings:diff.
fcode diff # the whole workspace
fcode processes:diff my-process # one process
Use it before resolving a 🔥 (conflict) from pull / push, so the user sees
what each side would lose before choosing. Notes:
- Exits
1when local and cloud differ,0when in sync — usable as a check. A non-zero exit here is the answer, not a failure to report as an error. - Sensitive variable values are never printed; both sides render as
<sensitive value hidden>, so a changed secret shows instatusbut not here. - Inherited resources follow the same rule as
status: the ones the cloud agrees on are hidden behind a count, and any a parent has moved are diffed, marked with the owning workspace. Fix those withfcode pull, never a push — they stay read-only. Inherited variables diff asvariables.inherited.env, the generated file they live in.
fcode remote:add <workspace-slug>
Points an already-cloned workspace folder at a different cloud workspace, so
subsequent fcode pull / push sync with that one instead — how code is
promoted between workspaces (e.g. dev → prod). Don't use it ad hoc: the gated
promotion procedure is in fcode-release.
fcode team:clone / team:pull / team:status
Work on every App a development team owns at once — the team of humans in the
dashboard, not the workspace sense of the word (see fcode-ama). team:clone
with no argument lists the development teams you belong to so you can copy an id;
with exactly one, it goes ahead:
fcode team:clone # lists your teams, or clones the only one
fcode team:clone <teamId>
It creates one folder per App, holding the App's metadata files and a checkout of that App's workspace:
acme-payroll/
┣ 📂 .fcode/team.json # the team and the Apps cloned into it
┣ 📂 .claude/skills/ # installed once, symlinked into every App below
┗ 📂 payroll-sync/
┣ 📜 settings.json # the App — name, description, id (mirror)
┣ 📜 README.md # the App's README — yours, pushed with `fcode app:push`
┣ 📜 MARKETPLACE.md # the marketplace listing — yours, pushed with `fcode app:push`
┣ 📜 CHANGELOG.md # generated from the App's releases (mirror)
┣ 📂 marketplace/logo.* # the App logo set in the dashboard (mirror)
┗ 📂 app/ # a normal workspace — settings.json, processes/, …
The checkout is called app, never the dev-<token> slug it came from: the slug
is in the metadata, and the directory is only a checkout — fcode remote:add can
re-point it at the prod workspace. Each level's settings.json describes the
thing that level holds, and they never collide, being one directory apart.
Inside an App's workspace every ordinary command works as usual; the team commands only add the ones that span Apps:
team:pullre-reads the team: it clones Apps added since the last run, pulls every workspace, and refreshes each App's metadata files. An App that left the team is reported, never deleted — remove the folder yourself if you want it gone.team:statusreports the state of each App'sREADME.mdandMARKETPLACE.md(up to date, modified locally, out of date, conflict), then runsfcode statusin the App workspace under its own header.- There is no
team:push. Code is pushed from inside a workspace, one App at a time; the App's README and listing go up withfcode app:push(below). - The App folder's files come in two kinds. Mirrors —
settings.json,CHANGELOG.md,marketplace/logo.*— are rewritten on every pull; editing them changes nothing upstream (rename an App in the dashboard, release notes are the comments given when a release is requested). Yours —README.mdandMARKETPLACE.md— are kept when edited: a pull only overwrites a file you have not changed since the last sync, and reports the ones it kept. - A failing App doesn't abort the run: it is listed in the summary, and rerunning
team:pullretries only what is still missing. A metadata file the dashboard could not serve is left as it was and reported as a warning — it never fails the App's clone.
fcode app:push
Uploads the App folder's README.md and MARKETPLACE.md to the dashboard. Run it
from the App folder or anywhere inside its app/ checkout — it recognises the
folder by the settings.json + app/ pair. It moves documentation only, never
code.
cd payroll-sync
fcode app:push # uploads the files that changed since the last sync
fcode app:push --force # local wins over a cloud edit
README.mdis the App's README, also shown and editable on the App page's README tab in the dashboard. A blank file clears it; a cloud README that was never written comes down as a stub (# <App name>+ description) so the file exists to be edited.MARKETPLACE.mdis the Publication tab's listing: YAML front mattertagline,category_id(a DatoCMS category id),support_link,help_link(http(s)://ormailto:), then the full description (markdown, 1–5000 chars) as the body. Fields the dashboard owns —publication_state,datocms_id— appear as comments and are never pushed. A linked DatoCMS record still wins over these fields in the marketplace (seefcode-ama).- Conflicts: a file changed both locally and in the dashboard since the last
pull is refused.
--forcemakes the local file win; deleting the file and runningfcode team:pullmakes the dashboard win. A file that is only behind the cloud is skipped with a hint to pull. - A file the dashboard rejects is reported with the field and reason (e.g. a tagline over 140 characters); the other file is still pushed and the command exits non-zero.
- After a push the local file is rewritten in the canonical form a pull would
produce, so
team:statusreads it as up to date.
fcode settings:pull / settings:push / settings:status
Sync the workspace-level settings in settings.json on their own: settings:pull
writes the cloud settings into the file, settings:push applies the file, and
settings:status reports whether they changed locally, in the cloud, or both.
Plain fcode pull / fcode push include them too, running them last so a
referenced error-handler process slug resolves against processes that already exist.
The versions / aliases fields sync like the rest of the file: settings:push
diffs them against the cloud and applies the difference — publishing versions
listed locally but not yet in the cloud (carrying their comment), asking per
version before deleting one removed from the file (the deletion cascades;
--force skips the prompt), and creating, re-pointing or deleting aliases to
match. settings:status compares them too, and settings:pull refuses to
overwrite a settings.json with unpushed local edits.
fcode settings:versions:* / fcode settings:aliases:*
Workspace versioning publishes a version of the whole workspace: every
process and module the team owns gets a version with the same tag, and bare
module imports are pinned to it inside the published snapshots (model in
fcode-core-concepts). Releases normally happen from the web UI (team
settings → Versions tab) — these commands are the imperative equivalent, and
editing settings.json's versions / aliases then pushing is the declarative one.
Don't create versions or move stable unless explicitly asked.
fcode settings:versions:create v1.0.0 --comment "First stable release"
fcode settings:versions:list
fcode settings:versions:delete v1.0.0 # cascades; asks confirmation unless --force
fcode settings:aliases:set stable v1.0.0 # create or re-point; rollback = older tag
fcode settings:aliases:list
fcode settings:aliases:delete stable
settings:versions:createskips entities already carrying the exact tag and reports a per-entity summary (created / skipped / failed — the version's manifest), then pulls so theversions/<tag>/folders andsettings.jsonrefresh locally. Re-running the same tag after a partial failure only publishes what is still missing.settings:versions:deletecascades: every owned process/module version with the tag is deleted, together with the aliases, executions, and schedules referencing them.settings:aliases:setupserts — it creates the alias or re-points an existing one on every owned entity that has the target tag published (entities without it are skipped and reported). Re-pointingstableat an older tag is the rollback: every consumer pinned tostableswitches in one operation.
fcode i18n:*
i18n:pull / i18n:push / i18n:status / i18n:add <locale> /
i18n:remove <locale> / i18n:reset sync the workspace's translation files —
i18n/<locale>.yaml, plus the read-only, gitignored
i18n/<locale>.inherited.yaml that pull writes. Aggregate fcode pull /
push / status include locales already. File format, inheritance model, and
the internationalization workflow in fcode-i18n.
Process metadata — metadata.json
Each process folder holds processes/<slug>/metadata.json — the source of
truth for the process's name, description, tags, triggers, and settings. It
round-trips with push/pull: edit the file and fcode push to change these
settings in the cloud, no dashboard needed. Changes show as 🔺 modified in
fcode status.
| Field | Type | Meaning |
|---|---|---|
name |
string | Display name (defaults to the slug) |
description |
string, optional | Process description |
tags |
string[] | Tags (defaults to []) |
webhook |
object, optional | Webhook trigger: enabled (boolean) turns the process's webhook endpoint on; authMode (NONE | TEAM | CUSTOM) says how callers authenticate — public, inheriting the workspace webhookAuth from settings.json, or its own; auth ({ headerName?, variableKey }, only with CUSTOM) names the header and the team variable holding the expected token |
form |
object, optional | Form settings: enabled (boolean) is the Forms flag (see fcode-forms); appRole marks the process's role in a marketplace app: INSTALL, SETTINGS, USER_FACING_FORM, or UNINSTALL. Every enabled form is public — there is no access restriction to configure |
uiTrigger |
object, optional | Button inside the Factorial UI (see fcode-ui-triggers): enabled (boolean); locationId (string, required when enabled, ≤ 200 chars) names the Factorial location; label (string, may carry fcode.i18n("key") tokens); icon (string, allowlisted name); awaitResult (boolean) runs the process synchronously and shows its result instead of fire-and-forget |
{
"name": "Order sync",
"description": "Syncs Shopify orders into Factorial",
"tags": ["integration", "shopify"],
"webhook": {
"enabled": true,
"authMode": "CUSTOM",
"auth": { "variableKey": "SHOPIFY_WEBHOOK_TOKEN" }
},
"form": { "enabled": false }
}
A webhook that inherits the workspace configuration carries
"webhook": { "enabled": true, "authMode": "TEAM" }, and a public one only
"webhook": { "enabled": true }.
{
"name": "Connect your account",
"tags": ["setup"],
"form": { "enabled": true, "appRole": "INSTALL" }
}
Notes:
webhook.auth.variableKeystores only the variable name, never a token — so the file is safe to commit. The variable doesn't have to exist yet; until it does, every call to the webhook is rejected with403. Both plain and secret variables work.authMode: TEAMinheritswebhookAuthfromsettings.json. When that configuration is missing, the webhook rejects every call — it never reads as public. Through MCP this matters: an agent can setauthMode: TEAMbut there is no team-settings tool, so the configuration has to exist already (set it insettings.jsonandfcode settings:push).webhook.auth.headerNamedefaults toAuthorization, whose value must beBearer <token>; any other header carries the raw variable value. Valid names are RFC 7230 token characters, at most 64 of them, andCookie,Hostand theFcode-prefix are rejected. Prefer the default: a bespoke header loses the redaction proxies and log pipelines giveAuthorization. Use one only when the sender can't setAuthorization— Factorial's own webhook sender, which puts its token inx-factorial-wh-challenge, is the case in point.form.appRoleandwebhook.authModeare omitted when unset orNONE, as iswebhook.auth.headerNamewhen it isAuthorization, so a form carries only"form": { "enabled": true }. To lift protection from a protected webhook, write"authMode": "NONE"explicitly — omitting the field leaves the stored mode untouched, and sendingauthwithoutauthMode: CUSTOMis rejected.- Omit
form.appRoleunless the process belongs to a marketplace app. fcode pullwrites"uiTrigger": { "enabled": false }on every process;locationId,labelandiconappear only when set, andawaitResultonly whentrue. Enabling a trigger without alocationIdis rejected on push.- If
metadata.jsonis missing,fcode addscaffolds{ "name": "<slug>", "tags": [] }; invalid JSON falls back to those defaults with a warning.
Calling a webhook — pin the version in the URL
The webhook endpoint is
https://code.factorialhr.com/platform/api/<team-slug>/webhooks/<process-slug>.
Always pin the version with the version_tag query parameter, pointing at the
stable alias — subscription systems rarely let you set request headers:
curl -X POST "https://code.factorialhr.com/platform/api/<team-slug>/webhooks/<process-slug>?version_tag=stable"
version_tagtakes a version tag (v1.0.0) or an alias. Usestable: it always exists, and releases/rollbacks then happen by moving the alias — the external system is never touched. It is equivalent to theFcode-Version-Tagheader and takes precedence over it.version_tag,asyncandlocaleare reserved names, stripped before the parameters reach the process (localeselects the execution's language — seefcode-i18n).- An unknown or malformed version does not fail the call. The process runs its current version and the platform only logs a server-side warning — a typo runs the current version silently. When a run behaves unexpectedly, check the execution's version.
Workspace settings — settings.json
A singleton file at the workspace root holding workspace-level settings. Synced by
fcode settings:pull / settings:push / settings:status, and included in plain
fcode push / pull (pushed last, so a referenced error-handler process
exists first).
| Field | Type | Meaning |
|---|---|---|
parentTeams |
array | Teams this workspace inherits processes, modules and variables from (direct parents only, max 5). Each entry is a plain slug (parent resolves live) or { slug, version } to pin that parent to one of its workspace versions — a tag or an alias |
zoneId |
string, optional | Team timezone (e.g. for schedules) |
errorHandlerConfig |
object, optional | { "processSlug": "<slug>", "tag": null } — process invoked when an execution errors; tag pins it to a version tag or alias (null = current version) |
webhookAuth |
object, optional | { headerName?, variableKey } — the configuration every authMode: TEAM webhook inherits |
primaryLocale |
string, optional | The workspace's main language: the locale used when a caller names none, and the key-level fallback for untranslated keys (see fcode-i18n) |
versions |
array, optional | Workspace versions: { tag, comment?, createdAt? } — declaring a new tag and pushing publishes it; omit createdAt, the cloud fills it in |
aliases |
array, optional | Workspace aliases: { name, tag } |
{
"parentTeams": ["shared-utils", { "slug": "base-app", "version": "stable" }],
"zoneId": "Europe/Madrid",
"errorHandlerConfig": { "processSlug": "error-handler", "tag": null },
"webhookAuth": {
"headerName": "x-factorial-wh-challenge",
"variableKey": "FACTORIAL_CHALLENGE_TOKEN"
},
"versions": [
{ "tag": "v1.0.0", "comment": "First stable release", "createdAt": "2026-08-01T10:00:00" }
],
"aliases": [{ "name": "stable", "tag": "v1.0.0" }]
}
The error handler is referenced by slug (not id) so settings.json is
portable across workspaces; the CLI resolves it to the cloud id on push.
A parentTeams entry is written as a plain slug while the parent resolves live,
and as { slug, version } once pinned — both forms are accepted on read, so a
hand-written list of slugs stays valid. The version is a tag or an alias of
that parent's workspace versions, and it must exist there or the push is
rejected. What pinning changes is in fcode-core-concepts; don't add or change
one unless explicitly asked.
versions and aliases are synced state, not a read-only mirror: they count
towards the content hash (edits show as modified in fcode status) and
settings:push applies them, with the diff semantics above. Only the set of tags and
the name → tag pairings are compared — a published version's comment can
no longer be changed, and createdAt is server-assigned.
webhookAuth is one shared configuration for the whole workspace, so a token used
by several webhooks is named — and rotated — in one place. Two things about it:
- It is per-workspace and not inherited through
parentTeams. Auth is resolved against the workspace addressed in the webhook URL, not the one that owns the code, so an app inheriting a webhook process from a base app still needs its ownwebhookAuthentry. - Removing the object and pushing clears the cloud configuration, which makes every webhook inheriting it reject all calls.
Inherited resources on disk
fcode pull writes everything inherited from parentTeams alongside the
workspace's own resources: variables.inherited.env,
i18n/<locale>.inherited.yaml, dependencies/package.inherited.json /
requirements.inherited.txt, and the inherited process/module folders inside
processes/ and modules/. The CLI keeps them parent-owned:
- Read-only — process/module files are written mode
444; needing achmodto edit is the signal to stop and change them in the owning workspace. - Gitignored via a CLI-managed block in
.gitignore, regenerated on every pull — don't hand-edit the block. fcode pushnever uploads them. One that was modified locally logs a warning naming the owning team — restore it withfcode processes:pull/modules:pull(or the aggregatefcode pull).- The
*:statuscommands list only the inherited resources that differ from the cloud (--showInheritedfor the rest), marking each with an inherited column naming the owning workspace (🔗 <slug>). - From a pinned parent you get that version's content, not the parent's
current code, and only the resources the pinned version published — a
process the parent added afterwards is not pulled at all. Changing the pin in
settings.jsonand pulling again is what moves those files.
Inheritance model (resolution order, overrides, pinning) in
fcode-core-concepts.
The three variables files
Team variables live in three .env files at the workspace root:
| File | Holds | Synced |
|---|---|---|
variables.env |
The variables this workspace owns | Committed; pushed and pulled |
variables.inherited.env |
The variables inherited from parent workspaces (parentTeams) |
Pull-only; gitignored (the CLI adds the entry) |
variables.local.env |
Local-only overrides | Never pushed, never pulled |
Resolution order for a local run (highest wins), matching what the cloud
does: variables.local.env → variables.env → variables.inherited.env.
Precedence is decided by which file declares a key, not by its value — so
blanking a key in variables.env overrides the inherited variable with an empty
string rather than falling through to the parent.
Overriding an inherited variable
Adding the key to variables.env is the override. From that point the CLI
treats it as this workspace's own variable: fcode status shows it as new,
fcode push creates it here, and fcode variables:add offers it.
- Don't copy a parent's variables into a child workspace to "make them
available" — they already resolve. Only add a key when this workspace genuinely
needs a different value. (Workspaces provisioned before inheritance existed may
still hold such copies, which now shadow the parent — including untouched
********placeholders shadowing a secret that would otherwise resolve. Flag those to the user rather than deleting them.) - Deleting your override (removing the key from
variables.envand pushing) brings the parent's value back.
Model and web-UI behaviour in fcode-core-concepts; the runtime
fcode.variables behaviour in fcode-javascript / fcode-python.
The file names are settings (variablesFileName,
inheritedVariablesFileName, localVariablesFileName) — assume the defaults
above unless the workspace says otherwise.
Variable sensitivity — variables.meta.json
A workspace-root file mapping each variable to its sensitivity flag:
{
"ACME_API_KEY": { "isSensitive": true },
"ACME_BASE_URL": { "isSensitive": false }
}
- Create a sensitive variable with
fcode variables:add --sensitive(then set its value and push); the flag lands here. - Variables created at runtime with
fcode.variables.setare sensitive by default — passsensitive: false(JS) /sensitive=False(Python) for plain config. Seefcode-javascript/fcode-python. - Sensitive values never leave the cloud:
pullwrites the placeholder********intovariables.env— and intovariables.inherited.envfor an inherited secret. Don't replace the placeholder in either file — put the real value invariables.local.envfor local runs. Remotely, an inherited secret's real value is available to executions (seefcode-core-concepts); only the local copy is masked. isSensitiveis immutable once pushed. Editing it invariables.meta.jsonis rejected on push (🚫 infcode status) — revert to match remote.
Getting secret values for local runs
When a local run (fcode run, a discovery script) needs a real secret value
that isn't in variables.local.env yet, ask the user to provide it. If they
prefer not to share the value with the agent, ask them to add the
KEY=value line to variables.local.env themselves — local runs pick it up
without the value ever appearing in the conversation.
variables.local.envvalues are never pushed. Remind the user to also create those secret variables manually in the remote demo environment —fcode pushwon't carry the values.FACTORIAL_TOKEN: needed locally only — the remote environment populates it automatically, so don't create it there. To obtain it, the user completes the OAuth flow in the Factorial Code app details page, then copies the generated token with the copy dropdown option in the OAuth Dev app, and puts it invariables.local.env(or shares it, per their preference). To run in a specific installed company's context instead, copy thatdeploy-workspace's token with Copy FACTORIAL_TOKEN in the "…" menu on the Test marketplace app page — same file, same handling.- Once obtained, never echo secret values back in output or logs.
Examples
Development cycle (new process):
fcode add
fcode dependencies:install # if dependencies changed
fcode run shopify-order-sync --parameters '{"dateFrom":"2024-01-01","dateTo":"2024-01-31"}'
fcode push # no need to re-run `add` if nothing new was created
Deploy an existing, tested process:
fcode push # `add` not needed — process already registered