Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Validate skills

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22

- name: Validate every skill with skills-ref
run: |
for dir in skills/*/; do
npx -y skills-ref validate "$dir"
done

- name: Check fcode-javascript and fcode-python stay mirrored
run: |
extract_headings() {
awk '/^```/{in_code=!in_code; next} !in_code && /^##/' "$1"
}
if ! diff \
<(extract_headings skills/fcode-javascript/SKILL.md) \
<(extract_headings skills/fcode-python/SKILL.md); then
echo "::error::fcode-javascript and fcode-python section headings diverged — semantic changes must land in both skills."
exit 1
fi
42 changes: 42 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Contributing

## Content ownership

Every concept has exactly **one owning skill** that holds its full explanation.
Any other skill that needs the concept gets at most a one-line gotcha plus a
pointer to the owner (`see fcode-<owner>`). When documenting a new platform
feature, write it up once in the owner and add pointers elsewhere — do not
repeat the explanation across skills.

| Concept | Owner |
|---|---|
| Platform model: processes, modules, execution context, variables & inheritance model, datastore vs storage, versioning & alias model | `fcode-core-concepts` |
| CLI commands and flow, `metadata.json` / `team.json` field reference, webhook auth mechanics, the three variables files, `variables.meta.json`, the `FACTORIAL_TOKEN` OAuth procedure, `version_tag` on URLs | `fcode-cli` |
| Language usage: runtime helpers, code snippets, logging, dependencies, return values | `fcode-javascript` / `fcode-python` |
| `parametersSchema.json` field types, widgets, validation | `fcode-json-schema` |
| Form embedding, themes, access restriction, pre-render / pre-fill contract, i18n, multi-step | `fcode-forms` |
| Agent working method, MCP tools | `fcode-agent` |
| Worked, adaptable examples (code, not rules — rules live with their owner) | `fcode-examples` |

## The JavaScript / Python twins

`fcode-javascript` and `fcode-python` are deliberate mirrors: the same
sections, the same semantics, language-specific idiom. **Every semantic change
must land in both files.** CI fails when their section headings diverge.

## Validation

CI validates every skill with
[`skills-ref`](https://github.com/agentskills/agentskills/tree/main/skills-ref)
on each pull request. Run it locally with:

```bash
for dir in skills/*/; do npx skills-ref validate "$dir"; done
```

## Frontmatter descriptions

The `description` field is the routing surface agents use to pick a skill.
Keep it to what the skill covers plus when to use it — a couple of sentences
with the trigger words that matter, not an exhaustive feature list. Don't
append keywords to it with every feature PR.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Each skill is a directory under `skills/` containing a `SKILL.md`
npx skills-ref validate skills/fcode-javascript
```

CI validates every skill on each pull request. Content-ownership rules for
contributors are in [CONTRIBUTING.md](CONTRIBUTING.md).

## License

[MIT](LICENSE)
12 changes: 5 additions & 7 deletions skills/fcode-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ Work in small steps, confirming at each one.
Share results.
- When a secret value is needed for discovery/testing, ask the user for it —
or, if they prefer not to share it, ask them to put it in
`variables.local.env` themselves (see `fcode-cli`). For `FACTORIAL_TOKEN`,
point them to the OAuth flow in the Factorial Code app details page and the
copy dropdown option in the OAuth Dev app.
`variables.local.env` themselves. For `FACTORIAL_TOKEN`, follow the OAuth
procedure in `fcode-cli`.
- Remind the user that local secret values aren't pushed — they must create
those variables manually in the remote demo environment (except
`FACTORIAL_TOKEN`, which is auto-populated remotely).
Expand All @@ -73,10 +72,9 @@ Then offer next steps: scheduling, webhooks (public, inheriting the workspace
`webhookAuth` configuration, or with their own header and team variable), a form
(public, or restricted to Factorial users), or exposing the process as an MCP
tool — and pushing to cloud when ready. Pushing only updates the current code:
consumers pinned to the `stable` alias (as webhook URLs and form embeds should
be) are unaffected until a release moves the alias, which is done from the web
UI — don't create workspace versions or move `stable` unless explicitly asked
(see `fcode-core-concepts` and `fcode-cli`).
consumers pinned to the `stable` alias are unaffected until a release moves the
alias — don't create workspace versions or move `stable` unless explicitly
asked (model in `fcode-core-concepts`, commands in `fcode-cli`).

## Creating MCP tools

Expand Down
8 changes: 5 additions & 3 deletions skills/fcode-agent/references/example-interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ notifications with order details to customers"
**Agent:** "I'll help you create this integration. First, for sending emails,
would you prefer:

1. **Email service** (SendGrid, AWS SES) — easier setup, better deliverability
2. **Direct SMTP** — more control, no third-party needed
1. **Built-in `fcode.sendMail`** — zero setup, but a fixed From address and a
cap of 3 emails per execution (see `fcode-javascript` / `fcode-python`)
2. **Email service** (SendGrid, AWS SES) — custom From, better for volume
3. **Direct SMTP** — more control, no third-party needed

Which approach?"

**User:** "SendGrid"
**User:** "SendGrid — there can be many orders per run"

**Agent:** "Perfect! Here's the plan:

Expand Down
2 changes: 1 addition & 1 deletion skills/fcode-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: fcode-cli
description: Use the Factorial Code CLI (fcode) for local development and cloud sync — the pull → add → dependencies:install → run → push flow, the local webhook/forms server (fcode http), workspace versions and aliases (team:versions, team:aliases, the stable alias, version_tag on webhook URLs), when to run each command, --force safety, worked examples, and the workspace config files (process metadata.json with webhook authMode/auth and form authMode/appRole, team.json with the workspace webhookAuth and versions/aliases, the three variables files including inherited variables from parent workspaces, variables.meta.json). Use when running fcode CLI commands, testing a Factorial Code process locally, deploying/syncing Factorial Code (fcode) resources to the cloud, managing workspace versions or aliases, overriding an inherited team variable, or activating and protecting a process's webhook or form settings.
description: Use the Factorial Code CLI (fcode) for local development and cloud sync — the pull → add → run → push flow, the local webhook/forms server (fcode http), workspace versions and aliases (the stable alias, version_tag), and the workspace config files (process metadata.json, team.json, the three variables files, variables.meta.json). Use when running fcode CLI commands, testing a process locally, syncing to the cloud, managing versions or aliases, overriding an inherited team variable, or configuring a process's webhook or form settings.
license: MIT
metadata:
category: factorial-code
Expand Down
8 changes: 8 additions & 0 deletions skills/fcode-examples/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ reimplement them:
Integration apps inherit base-integration-app (which inherits base-app);
custom apps inherit base-app directly.

Note: the `fcode-forms` **module** above (form-schema builders you
`fcode.import`) is unrelated to the `fcode-forms` **skill**, which documents
embedding forms on webpages.

The references keep code compact and omit routine logging; real
implementations should log verbosely through the shared `fcode-logs` module
(see `fcode-javascript` / `fcode-python`).

## Language variants

Every sample exists in JavaScript (Node.js v22) and Python (3.13) with
Expand Down
20 changes: 8 additions & 12 deletions skills/fcode-examples/references/custom-app-linear.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ module.exports = { LinearApiClient };

## Setup step 1 — validate credentials, chain to step 2

A minimal form (one password field, `"isSensitive": true`,
`"ui:widget": "password"`). The process validates the key with a real API
call, persists it as a team variable, and chains by returning
A minimal form (one credential field with `"isSensitive": true`, which renders
a password input — see `fcode-json-schema`). The process validates the key
with a real API call, persists it as a team variable, and chains by returning
`nextProcessId`:

```javascript
Expand Down Expand Up @@ -380,16 +380,12 @@ await fcode.datastore.set(
);
```

Two rules this encodes:
The rules this encodes — a secret is reported, never echoed; blank on submit
means "keep the current value"; the credential field is not `required`; a
pre-render defaults missing state instead of throwing — are the pre-fill
contract in `fcode-forms` (`references/advanced.md`).

- **Secrets are reported, never echoed.** The schema response travels over HTTP
and lands in the browser DOM; `authMode: FACTORIAL` limits *who* sees it, not
*where it goes*. Show "configured", never the value — not even masked, since a
masked value then has to be told apart from a real one on submit.
- **A pre-render failure fails the form load.** Default missing state (`?? 1`,
`|| "{}"`) instead of throwing.

That second rule changes the install-flow snippet above. Throwing on a missing
The no-throw rule changes the install-flow snippet above. Throwing on a missing
`LINEAR_API_KEY` is fine while the form is only ever reached from step 1, which
just stored it — but the moment the same form is also the `SETTINGS` screen, that
throw makes it unopenable in precisely the state where the user needs it to enter
Expand Down
13 changes: 6 additions & 7 deletions skills/fcode-examples/references/integration-acme.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ Factorial-triggered webhook expects `FACTORIAL_CHALLENGE_TOKEN` in the
}
```

Set `webhookAuth` in **every** app workspace: it is not inherited through
`parentTeamSlugs`, because auth resolves against the workspace addressed in the
webhook URL rather than the one owning the code. Field reference in `fcode-cli`.
Set `webhookAuth` in **every** app workspace — it is not inherited through
`parentTeamSlugs` (why, and field reference, in `fcode-cli`).

The platform authenticates the caller before the process runs, so there is
nothing to check in code and the body stays minimal:
Expand Down Expand Up @@ -264,10 +263,10 @@ Vendor variables follow the `VENDOR__KEY` convention (matches
`variableName()`).

`FACTORIAL_TOKEN` is auto-populated in the remote environment — don't create
it there. For local runs, obtain it via the OAuth flow in the Factorial Code
app details page (copy dropdown in the OAuth Dev app) and put it in
`variables.local.env`. Other sensitive variables must be created manually in
the remote demo environment — local values aren't pushed (see `fcode-cli`).
it there. For local runs, obtain it via the OAuth flow and put it in
`variables.local.env` (procedure in `fcode-cli`). Other sensitive variables
must be created manually in the remote demo environment — local values aren't
pushed (see `fcode-cli`).

## Adapting to a real vendor — checklist

Expand Down
11 changes: 4 additions & 7 deletions skills/fcode-forms/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: fcode-forms
description: Embed a Factorial Code process's input-parameter form on a webpage — the three embed methods (data attributes, Fcode.initForm, FcodeForm React component) addressed by team + process slug and pinned to a process version (the stable alias, data-fcode-form-process-version / processVersion), restricting access with authMode, driving behavior from the process return value (message/formErrors/redirect/nextProcessId), styling and the two themes (the SDK stylesheet theme vs the f0 theme for React apps inside Factorial), i18n, multi-step flows, pre-rendering current values into install/settings forms, and automatic file uploads to Storage. Use when embedding, configuring, styling, theming, restricting access to, version-pinning, pre-filling, or wiring up submission callbacks for a Factorial Code (fcode) form.
description: Embed a Factorial Code process's input-parameter form on a webpage — the three embed methods (data attributes, Fcode.initForm, the FcodeForm React component), version pinning to the stable alias, access restriction (authMode), driving behavior from the process return value, styling and themes (including the f0 theme inside Factorial), i18n, multi-step flows, pre-filling current values, and file uploads. Use when embedding, configuring, styling, restricting, version-pinning, pre-filling, or wiring up a Factorial Code (fcode) form.
license: MIT
metadata:
category: factorial-code
Expand All @@ -24,10 +24,8 @@ handled in-page (messages, redirects, callbacks). For the schema itself, see
(`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 loads and
submits on the current version, with only a server-side warning. A typo in the
version attribute is invisible in the page; check the execution's version when
a submission behaves unexpectedly.
- **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.
Expand Down Expand Up @@ -288,8 +286,7 @@ 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
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:

Expand Down
7 changes: 7 additions & 0 deletions skills/fcode-javascript/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const { id: processId, name: processName } = fcode.execution.process;
const { id: scheduleId } = fcode.execution.schedule; // when run from a schedule
const timezone = fcode.execution.timezone;

// Workspace (team) metadata
const teamSlug = fcode.team.slug;

// Environment variables (secrets/config)
const apiKey = process.env.API_KEY; // or fcode.env.API_KEY

Expand Down Expand Up @@ -123,6 +126,9 @@ await fcode.storage.upload("path/myfile.txt", fs.createReadStream(localPath));
const files = await fcode.storage.list();
const stream = await fcode.storage.download("path/myfile.txt");
stream.pipe(fs.createWriteStream(localPath));
// Signed download URL — { url, expiresAt }. A real HTTPS link in the cloud,
// a file:// URL locally (same shape, no special-casing).
const signed = await fcode.storage.createSignedUrl("path/myfile.txt");
await fcode.storage.delete("path/myfile.txt");
```

Expand Down Expand Up @@ -161,6 +167,7 @@ await fcode.variables.delete("API_KEY"); // no-op on an inherited variabl
const schedule = await fcode.schedule.create("my-process", {
cron: "0 0 6 * * SUN", // or: dateTime: "2026-04-24T12:30:00.000"
input: { parameters: { foo: "bar" } },
allowConcurrentExecutions: false, // optional
});
const schedules = await fcode.schedule.list({
processId: fcode.execution.process.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"onePasswordField": {
"title": "One password field",
"type": "string",
"description": "Password shoul be: \n 1. At least 12 characters long \n 2. Include a combination of uppercase and lowercase letters \n 3. At least one special character such as @, #, $, %",
"description": "Password should be: \n 1. At least 12 characters long \n 2. Include a combination of uppercase and lowercase letters \n 3. At least one special character such as @, #, $, %",
"isSensitive": true,
"ui": {
"ui:placeholder": "Use a secure password"
Expand Down
8 changes: 8 additions & 0 deletions skills/fcode-python/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ process_id = fcode.execution.process.id
schedule_id = fcode.execution.schedule.id # when run from a schedule
timezone = fcode.execution.timezone

# Workspace (team) metadata
team_slug = fcode.team.slug

# Environment variables (secrets/config)
api_key = os.getenv("API_KEY") # or fcode.env.API_KEY

Expand Down Expand Up @@ -127,6 +130,10 @@ content = fcode.storage.download("path/myfile.txt")
with open(local_path, "wb") as f:
f.write(content)

# Signed download URL — { "url", "expiresAt" }. A real HTTPS link in the
# cloud, a file:// URL locally (same shape, no special-casing).
signed = fcode.storage.create_signed_url("path/myfile.txt")

fcode.storage.delete("path/myfile.txt")
```

Expand Down Expand Up @@ -163,6 +170,7 @@ schedule = fcode.schedule.create(
"my-process",
cron="0 0 6 * * SUN", # or: date_time="2026-04-24T12:30:00.000"
parameters={"foo": "bar"},
allow_concurrent_executions=False, # optional
)
schedules = fcode.schedule.list(process_id=fcode.execution.process.id)
fcode.schedule.pause(schedule["id"])
Expand Down