fcode-forms

Helps developers embed and style input forms from Factorial Code processes on websites.

Installation
Run `npx skills add "https://github.com/factorialco/factorial-code-skills" --skill "fcode-forms"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

SKILL.mdShow the author's original SKILL.md
---
name: fcode-forms
description: Embed a Factorial Code process's input-parameter form on a webpage — embed methods, version pinning to the stable alias, access restriction, driving behavior from the process return value, styling and themes, multi-step flows, and file uploads. Use when embedding, configuring, styling, or wiring up a Factorial Code (fcode) form.
license: MIT
metadata:
  category: factorial-code
---

# Factorial Code — Forms

A Factorial Code Form embeds a process's input-parameter form on any webpage.
Each submission starts a process execution with the form data, and the result is
handled in-page (messages, redirects, callbacks). For the schema itself, see
`fcode-json-schema`.

## Gotchas

- **The form *is* the process's `parametersSchema.json`** — there is no separate
  form definition. To change fields/validation/labels, edit the schema, **not**
  the embed code.
- **`team` and `process`/`processId` are both mandatory** on every embed, and
  both take **slugs** — not the per-workspace UUIDs.
- **Always pin the embed to the `stable` alias**
  (`data-fcode-form-process-version="stable"` / `processVersion: "stable"`).
  An unpinned form runs the current version, so every `fcode push` changes it
  immediately. Details below.
- **An unknown version or alias doesn't fail the form** — it silently runs the
  current version (see "Pin the form to a version").
- **The `Forms` flag must be enabled** — on the process Dashboard, or via
  `"form": { "enabled": true }` in the process's `metadata.json` + `fcode push`
  — or the embed won't render.
- **A form is public unless you say otherwise** (`authMode` absent = `NONE`).
  A form opened from inside Factorial (marketplace `INSTALL` / `SETTINGS` /
  `USER_FACING_FORM` / `UNINSTALL` screens) should carry `authMode: FACTORIAL`
  explicitly (below).
- **A form opened from a UI trigger button must stay public for now** — the
  trigger dialog sends no user token yet. See `fcode-ui-triggers`.
- **A schema can't carry executable JavaScript.** `embedFormOptions.onChange`
  and field `transformFn` were removed, and messages are markdown — raw HTML is
  never rendered. Client-side behaviour lives in the embedding page.
- **Never put secrets in embed code or `options`** — they run in the browser.
- **Form text is translated with `fcode.i18n("key")` tokens in the schema**,
  substituted server-side before the schema is served. See `fcode-i18n`.
- **Form submissions run under a request timeout** (about a minute) — keep the
  synchronous process fast, or run long work asynchronously (see below).
- Prefer driving UX from the **process return value** (below); reserve
  `onSuccess`/`onError` for client-only logic.

## Enable a form

1. Create the process and define its input parameters (these become the fields).
2. Enable the `Forms` flag — either on the process Dashboard, or from the CLI
   workspace in `processes/<slug>/metadata.json`, then `fcode push`:

```json
{
  "name": "Contact request",
  "tags": [],
  "form": { "enabled": true }
}
```

For marketplace app processes, `form` also takes an optional
`"appRole"` (`INSTALL` | `SETTINGS` | `USER_FACING_FORM` | `UNINSTALL`) marking
the process's role in the app. Field reference in `fcode-cli`.

An `INSTALL` or `SETTINGS` form is re-opened after the app is configured, so it
should show the **current** values rather than an empty form — the
`preRenderProcess` pattern for that is in `references/advanced.md`.

Read submitted values in process code like any parameters:
`const { context: { parameters } } = fcode;`

## Restrict who can open the form

The `Authentication` field next to the `Forms` flag (`form.authMode` in
`metadata.json`) decides who may read the form schema **and** submit it:

| `authMode` | Who gets in |
|---|---|
| `FACTORIAL` | Only Factorial users of the company that installed the app. Every request must carry a Factorial-issued user token in the `Fcode-Factorial-Token` header, and that token's company must own the workspace. Anything else gets a `401` |
| `NONE` | Anyone who knows the form URL can open and submit it |

- **Forms are public by default** — enabling one without an `authMode` leaves it
  reachable by anyone with the URL. Requiring a Factorial user is an explicit
  opt-in: set `authMode: FACTORIAL` on every form that runs app code against
  customer data from inside Factorial.
- Forms embedded **inside Factorial** (the marketplace `INSTALL` / `SETTINGS` /
  `USER_FACING_FORM` / `UNINSTALL` screens) send the token for you — this is what
  `FACTORIAL` is for.
- A protected form is still openable from the **playground link** on the process
  Dashboard: the playground sends the developer's own Factorial Code token as
  `Fcode-Platform-Token` and access is granted through workspace membership.

Field encoding rules (`authMode` omitted when `NONE`, lifting protection needs
an explicit `"authMode": "NONE"`) and the full reference in `fcode-cli`.

## Embed a form

Two mandatory inputs, both **slugs**, plus the version pin you should always
add:

- **`fcode-team-slug`** — from `https://code.factorialhr.com/platform/<fcode-team-slug>`
- **`fcode-process-slug`** — the **Slug** field on the process Dashboard (e.g.
  `send-welcome-email`)
- **process version** — pin it to the `stable` alias (next section)

**Use the slug, not the process ID.** IDs are per-workspace UUIDs, so an
id-based embed breaks when the snippet moves between workspaces; slugs survive
(existing id-based embeds keep working, and the React prop is still named
`processId` — feed it a slug). Slugs are editable with no redirect for the old
value, so settle them before handing out embed code — renaming one breaks every
embed already pasted into a page.

Load the SDK once (needed for the data-attribute and `Fcode.initForm` methods):

```html
<script defer src="https://code.factorialhr.com/sdk/forms.js"></script>
```

**Method 1 — data attributes** (SDK replaces the element):

```html
<div
  data-fcode-form-team="<fcode-team-slug>"
  data-fcode-form-process="<fcode-process-slug>"
  data-fcode-form-process-version="stable"
></div>
```

**Method 2 — `Fcode.initForm`** (selector or DOM element):

```html
<div id="my-fcode-form"></div>
<script>
  Fcode.initForm("#my-fcode-form", {
    team: "<fcode-team-slug>",
    process: "<fcode-process-slug>",
    processVersion: "stable",
  });
</script>
```

**Method 3 — `FcodeForm` React component** (React 17/18; install
`@factorialco/fcode-react-forms`):

```jsx
import FcodeForm from "@factorialco/fcode-react-forms";

const MyComponent = () => (
  <FcodeForm
    team={"<fcode-team-slug>"}
    processId={"<fcode-process-slug>"}
    processVersion={"stable"}
  />
);
```

In SSR frameworks (e.g. Next.js), import it dynamically with `ssr: false`.

## Pin the form to a version

The version pin (`data-fcode-form-process-version` / `processVersion`) takes a
published version tag (`v1.0.0`) or an alias — use `stable`, so releases and
rollbacks happen by moving the alias, never by editing the embedded page
(alias model in `fcode-core-concepts`; release commands in `fcode-cli`). The
pin applies to **both** requests the form makes — loading the definition and
submitting it — and the process Dashboard writes it for you: pick a version or
alias in the selector next to the embed code and copy the generated snippet.

**An unknown version or alias silently runs the current version** — the
platform only logs a server-side warning, same as webhooks (see `fcode-cli`).
A typo in the attribute is invisible: check the execution's version when a
submission behaves unexpectedly. Direct calls to the form endpoints also accept
the `version_tag` query parameter documented in `fcode-cli`.

## Handle submission results

Default: a loading overlay shows during execution; on success the form is
replaced with a success message, on error an error message.

**Callbacks** (same shape across methods):

```js
Fcode.initForm("#my-fcode-form", {
  team: "<fcode-team-slug>",
  process: "<fcode-process-slug>",
  onSuccess: (formId, processExecutionResult, formSubmittedData) => {},
  onError: (formId, error, formSubmittedData) => {},
});
```

With data attributes, point to global functions via
`data-fcode-form-on-success="HANDLER_NAME"`,
`data-fcode-form-on-next-step="..."` and `data-fcode-form-on-error="..."`.

**Drive behavior from the process return value** (no client code needed):

```js
return { message: "Thanks, **we received your request**." };                // success message (markdown)
return { message: "| Item | Status |\n|---|---|\n| Sync | Done |" };        // GFM tables work too
return { status: 400, body: { formErrors: {                                 // inline validation errors
  fields: { email: "Invalid email." }, global: ["A global error."] } } };
return { status: 400, body: { errorMessage: "**Sync failed** — retry." } }; // error message (markdown)
return { redirect: { url: "https://example.com", timeout: 2000 } };         // redirect after submit
```

**Messages are markdown, not HTML.** `message`, `errorMessage` and a schema's
`markdown.before`/`markdown.after` blocks render as GitHub Flavored Markdown —
tables, headings, links, images, code blocks, task lists. Raw HTML is
**dropped, never rendered** (so nothing authored can execute); put behaviour in
the success / next-step / error callbacks instead.

## Keep it fast, or go async

The submission waits for the process to finish, under a request timeout (about a
minute). Heavy work done inline — slow API calls, large exports, multi-record
syncs — will blow the timeout and fail the submit.

**Go async when the work can be slow:**

- **Embed `async: true`** — the submission returns `201` + an execution ID
  immediately instead of waiting for the result (see `references/advanced.md`).
- **Hand off to another process** — kick off the heavy work with
  `fcode.processes.run("process-identifier", options)` (see `fcode-javascript` /
  `fcode-python`) and return a quick acknowledgement (`message`/`redirect`)
  rather than awaiting it inline.

**Stay synchronous only when** the request is genuinely fast, or when data must
flow between steps. For passing data, don't block the submit — instead:

- **`preRenderProcess`** computes server-side `variables` before the form
  renders (see `references/advanced.md`).
- **Chained multi-step forms** carry state forward via `nextProcessId` +
  `variables` (below). If the steps are only visual — no work between them —
  use `ui:steps` instead and keep a single process.

## Multi-step forms

Two mechanisms, and picking the right one matters:

| Mechanism | Processes | Executions | Use when |
|---|---|---|---|
| **`ui:steps`** — steps within one form | one | one, after the last step | The split is purely visual; all the logic lives in one process |
| **`nextProcessId`** — chaining | one per step | one per step | An intermediate step must actually execute something (validate externally, compute `variables` for the next form) |

They compose: a chained process's form can itself declare `ui:steps`.

**When a form grows long — roughly ten fields or more — split it with
`ui:steps`**, grouping related fields per step with the required ones early.

### Steps within a single form (`ui:steps`)

Declare steps in the schema's root `ui` node, assigning each property to a step.
The SDK shows one step at a time with a "Next" button; only the **last** step's
button submits and starts the (single) execution, which receives every collected
parameter exactly as a one-page form would — process code needs no changes.

```json
"ui": {
  "ui:steps": {
    "config": { "layout": "tabs" },
    "steps": [
      { "title": "About you", "description": "Optional text", "fields": ["name"] },
      { "title": "Details", "fields": ["age", "subscribe"] }
    ]
  }
}
```

- `config` (optional): `layout` — `"tabs"` (default) or `"sidebar"`;
  `nextLabel` / `backLabel` — button labels, default `"Next"` / `"Back"`.
  `ui:submitButtonOptions` applies to the **last** step's button.
- "Next" validates only the fields on screen (including that step's share of
  `required`); a Back button and the step navigation revisit completed steps
  without losing input, and unreached steps stay disabled.
- Properties not listed in any step are appended to the last one; an unusable
  `ui:steps` falls back to the ordinary one-page form with a console warning.
- Supports root-level `properties` + `required` + `dependencies`; root-level
  `oneOf`/`allOf` compositions are not split into steps. SDK version
  requirements in `references/advanced.md`.

### Chaining processes (`nextProcessId`)

Each step is its own process. Return the next process's **slug** to advance:

```js
return { nextProcessId: "collect-shipping-address" };
```

The field name is still `nextProcessId` and it accepts a slug or an id — use the
slug, so the same chain works in every workspace.

The SDK then renders the form for `nextProcessId`. Each later step receives all
previous steps' data and results in `fcode.context.parameters` under a `steps`
array. Return a `variables` node alongside `nextProcessId` to pass state forward.

## Automatic file uploads

A file field is `"type": "string"` with a `"ui": { "ui:widget": "file" }` key
**inside the property**:

```json
{
  "properties": {
    "inputFile": { "type": "string", "ui": { "ui:widget": "file" } }
  }
}
```

(A root-level `ui` map keyed by field name — rjsf's `uiSchema` convention — is
also merged, but per-property `ui` is the documented form; the root `ui` is
mainly for form-level options like `ui:submitButtonOptions`. For secret inputs
prefer `"isSensitive": true` on the property — it renders a password widget
automatically; see `fcode-json-schema`.)

On submit the file is **uploaded to Storage before the process starts**, and the
parameter arrives as an `fcode.storage://…` reference (an **array** if multiple
files allowed). Strip the prefix to download:

```js
const { context: { parameters } } = fcode;
const stream = await fcode.storage.download(
  parameters.inputFile.replace("fcode.storage://", "")
);
```

Uploaded files count toward storage limits — delete them at the end of the
process if only needed transiently.

## Advanced

For styling and the two themes (including the f0 theme to use when embedding in a
React app inside Factorial), markdown message rendering with f0 components
(`markdownComponents`), initial/hidden values, async submission, custom
headers, API-host override, variables replacement, pre-rendering current values
into install/settings forms, reacting to user input from your own page (the
React `onChange` prop, the `fcode-forms-*` DOM events), and modal rendering,
read `references/advanced.md`.

Ships with 1 supporting file:

  • references/advanced.md

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us