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 changed
dependencies/package.json or dependencies/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 run before fcode add when the process (or other
resource) was just created — you'll hit "Local process not found".
fcode add is only for NEW resources. For edits to existing process/module
code or variables, go straight to fcode push.
--force (on push/pull) overwrites the other side. Both commands fail
when local and cloud diverge — settings:pull included, which refuses to wipe an unpushed settings.json. Run fcode diff first to show the user exactly what would be discarded, and only use --force with explicit user confirmation.
- Editing
versions / aliases in settings.json releases 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 as settings: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 to variables.env (a locale key to i18n/<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 in variables.local.env.
--auth-user / --auth-password consume the Authorization header, 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
1 when local and cloud differ, 0 when 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 in status but 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 with fcode pull, never a push — they stay read-only. Inherited variables diff as variables.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:pull re-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:status reports the state of each App's README.md and
MARKETPLACE.md (up to date, modified locally, out of date, conflict), then runs fcode status in 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 with fcode 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.md and MARKETPLACE.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:pull retries 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.md is 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.md is the Publication tab's listing: YAML front matter
tagline, categoryid (a DatoCMS category id), supportlink, helplink (http(s):// or mailto:), then the full description (markdown, 1–5000 chars) as the body. Fields the dashboard owns — publicationstate, datocms_id — appear as comments and are never pushed. A linked DatoCMS record still wins over these fields in the marketplace (see fcode-ama).
- Conflicts: a file changed both locally and in the dashboard since the last
pull is refused. --force makes the local file win; deleting the file and running fcode team:pull makes 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:status reads 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:create skips entities already carrying the exact tag and
reports a per-entity summary (created / skipped / failed — the version's manifest), then pulls so the versions/<tag>/ folders and settings.json refresh locally. Re-running the same tag after a partial failure only publishes what is still missing.
settings:versions:delete cascades: every owned process/module version with
the tag is deleted, together with the aliases, executions, and schedules referencing them.
settings:aliases:set upserts — 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-pointing stable at an older tag is the rollback: every consumer pinned to stable switches 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, USERFACINGFORM, 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.variableKey stores 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 with 403. Both plain and secret variables work.
authMode: TEAM inherits webhookAuth from settings.json. When that
configuration is missing, the webhook rejects every call — it never reads as public. Through MCP this matters: an agent can set authMode: TEAM but there is no team-settings tool, so the configuration has to exist already (set it in settings.json and fcode settings:push).
webhook.auth.headerName defaults to Authorization, whose value must be
Bearer <token>; any other header carries the raw variable value. Valid names are RFC 7230 token characters, at most 64 of them, and Cookie, Host and the Fcode- prefix are rejected. Prefer the default: a bespoke header loses the redaction proxies and log pipelines give Authorization. Use one only when the sender can't set Authorization — Factorial's own webhook sender, which puts its token in x-factorial-wh-challenge, is the case in point.
form.appRole and webhook.authMode are omitted when unset or NONE, as
is webhook.auth.headerName when it is Authorization, 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 sending auth without authMode: CUSTOM is rejected.
- Omit
form.appRole unless the process belongs to a marketplace app.
fcode pull writes "uiTrigger": { "enabled": false } on every process;
locationId, label and icon appear only when set, and awaitResult only when true. Enabling a trigger without a locationId is rejected on push.
- If
metadata.json is missing, fcode add scaffolds
{ "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_tag takes a version tag (v1.0.0) or an alias. Use stable: it
always exists, and releases/rollbacks then happen by moving the alias — the external system is never touched. It is equivalent to the Fcode-Version-Tag header and takes precedence over it. version_tag, async and locale are reserved names, stripped before the parameters reach the process (locale selects the execution's language — see fcode-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 own webhookAuth entry.
- 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 a
chmod to 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 push never uploads them. One that was modified locally logs a
warning naming the owning team — restore it with fcode processes:pull / modules:pull (or the aggregate fcode pull).
- The
*:status commands list only the inherited resources that differ from
the cloud (--showInherited for 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.json and 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.env and 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.set are **sensitive by
default** — pass sensitive: false (JS) / sensitive=False (Python) for plain config. See fcode-javascript / fcode-python.
- Sensitive values never leave the cloud:
pull writes the placeholder
******** into variables.env — and into variables.inherited.env for an inherited secret. Don't replace the placeholder in either file — put the real value in variables.local.env for local runs. Remotely, an inherited secret's real value is available to executions (see fcode-core-concepts); only the local copy is masked.
isSensitive is immutable once pushed. Editing it in
variables.meta.json is rejected on push (🚫 in fcode 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.env values are never pushed. Remind the user to also
create those secret variables manually in the remote demo environment — fcode push won'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 in variables.local.env (or shares it, per their preference). To run in a specific installed company's context instead, copy that deploy- 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