fcode-ui-triggers
Original:🇺🇸 English
Translated
Surface a Factorial Code process as a button inside the Factorial product UI — the `factorial.uiTrigger` settings in a process's metadata.json (location id, label, icon; `awaitResult` at the block's top level) and the legacy `uiTrigger` block they replace, what the process receives when a user clicks, what a synchronous trigger returns, form-backed triggers and their file uploads, i18n labels, the icon allowlist, and how Factorial's `FactorialCodeTrigger` component renders them. Use when an app process should appear as an action button on a Factorial page, or when wiring the button entry point of a Factorial Action ("Trigger from Factorial" in the console).
10installs
Added on
NPX Install
npx skill4agent add factorialco/factorial-code-skills fcode-ui-triggersTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Factorial Code — UI triggers
A UI trigger exposes a marketplace app's process as a button inside
Factorial (a page header, an actions dropdown, …). Factorial pages declare
locations; an installed app's process claims a location, and Factorial renders
one button per claiming process. Clicking it runs the process — or opens its
form. It is one entry point of a Factorial Action: its settings live in the
sub-block of the process's , with
at the top level of the block (the whole block, the
legacy mirror and the Factorial One / backend entry points are in
; field reference in ).
factorial.uiTriggermetadata.jsonawaitResultfactorialfcode-factorial-actionsfcode-cliGotchas
- Only installed marketplace apps get buttons. Factorial lists the triggers of the apps the company has installed, read from their deploy workspaces. A process in a plain workspace never shows up, and a trigger reaches customers the way the rest of the app does: dev → prod → deploy through a release.
- The location id must come from the Factorial team that owns the page.
Locations are declared in Factorial's code (e.g. ), not on the platform. The platform accepts any non-blank
calendar.header.admin(≤ 200 chars) — an unknown one is silently dropped from the listing, so a typo means "no button", not an error.locationId - The location decides the contract, not the process: which context params
are forwarded to the process, which keys of a synchronous result reach the
page, and whether the location is (one installed app at a time). Ask for those three things along with the id.
single - lives on the
awaitResultblock and defaults tofactorial(synchronous). The legacytruedefaulted to fire-and-forget, and the mirror copies the new value over it — so a button moved intouiTrigger.awaitResultruns synchronously unless you writefactorial. Keep it"awaitResult": falseonly for fast work (< 50 s) whose outcome the user must see; withtruethe click queues an execution, the user sees "action started" and the return value is never shown.false - A form-enabled process opens its form instead of running. With
(or the legacy
factorial.form.enabled) on, the button opens the form in a dialog inside Factorial andform.enabledis ignored.awaitResult - A trigger-opened form goes through Factorial, not straight to the
platform. The dialog reads the schema and submits it through Factorial's own
backend, which carries the clicking user's identity. The form's own
still governs anyone reaching it by its public URL (
authMode); it does not govern the dialog.fcode-forms - ,
company_idandtriggered_from_locationare trustworthy on both paths. Factorial injects them server-side on every invocation, form or not, and the location's forwarded params overwrite whatever the browser sent. Authorizing on them is safe. (This was not true of the old form path, which passed them as client-editable pre-filled fields.)access_id - Who may click is decided by , and it is enforced. Factorial checks
requiredPolicies(factorial.requiredPolicies) against the clicking user before it runs anything, on top of a policy-scoped read of the location's resource. An action with no policies listed is open to anyone who can reach the button.fcode-factorial-actions - Uncaught errors show a generic message. A throw / crash reaches the user as
"The action could not be completed" with no detail. Return for anything the user should read.
{ errors: [...] } - Changes take up to five minutes to appear — Factorial caches the trigger listing in the browser.
Declare a trigger
In , then (or the Trigger from
Factorial section of the process page in the console):
processes/<slug>/metadata.jsonfcode pushjson
{
"name": "Sync report",
"tags": ["acme"],
"factorial": {
"enabled": true,
"uiTrigger": {
"enabled": true,
"locationId": "compensations.cycle.header",
"label": "Sync to Acme",
"icon": "Refresh"
}
}
}| Field | Meaning |
|---|---|
| Master switch of the whole action; the button needs it on |
| Defaults to |
| Turns the button on. |
| The Factorial location the button renders at, as given by the page's owning team |
| Button text. Plain text, or |
| One of the allowlisted names (below); omit for a text-only button |
The legacy top-level block (, , ,
, ) still round-trips and is kept mirrored with
— an old file keeps working, and still
writes on every process. Write
in new work; when both are present sends (mirror rules
in ).
uiTriggerenabledlocationIdlabeliconawaitResultfactorial.uiTriggerfcode pull"uiTrigger": { "enabled": false }factorialfcode pushfactorialfcode-factorial-actionsAn app may declare several triggers at one location — each renders its own
button — but a location marked in Factorial admits one installed
app: installing a second app that claims it fails with a conflict naming the
first. Don't claim a location from an app meant to coexist with others.
singlesingleWhat the process receives
The click runs the process with set to:
fcode.context.parameters- the location's forwarded context params — e.g. the id of the record the
page shows (), exactly the keys the location allowlists;
cycle_id - — the Factorial company whose user clicked (trusted: injected under a shared secret, overwriting anything the browser sent);
company_id - — the location id (trusted, same way);
triggered_from_location - — the Factorial access (the clicking user's membership in that company) as a string (trusted, same way).
access_id
A context-less location (a header button with no record in scope) forwards no
params at all — only the three trusted keys. The clicking user's locale is passed
as the execution locale, so in the process speaks their language
().
fcode.i18nfcode-i18njavascript
const { company_id, access_id, triggered_from_location, cycle_id } = fcode.context.parameters;
if (!cycle_id) throw new Error("cycle_id is required");Return a result (synchronous triggers)
With Factorial waits for the execution and hands the page
whatever you returned, unchanged. There is no envelope to wrap it in, and
nothing is filtered out on the way.
awaitResult: truejavascript
// Success — the whole object reaches the page's onSuccess handler.
return { synced: 42, report_url: "https://acme.example/reports/7" };- A success status is always a success, whatever the body says. Returning an
object with an key in it does not make it a failure.
errors - Signal an expected failure with an error status, not with the body. Throw
, or return an error status from an HTTP-shaped process; the body travels intact so the page can read your message.
fcode.error(...) - A throw with no message, or exceeding the synchronous budget (about 50 s; the execution may still finish in the background) → a generic error the user can't act on.
Matching what the entry point expects is the process author's job: a form reads
its own conventions (, , ) while a plain
button just shows a toast, so a process exposed as both does not get one
portable result shape. on the location is informational — it
describes what the page intends to read, and nothing filters by it.
messageformErrorsnextProcessIdresult_keysWith off the return value is only visible in the execution log;
long work belongs there, or behind from a quick
synchronous trigger (see / ).
awaitResultfcode.processes.run(...)fcode-javascriptfcode-pythonForm-backed triggers
Enable the form as usual () and the button opens it in a dialog
inside Factorial, rendered with the f0 theme, pre-filled with the forwarded
params. Whether a click opens the dialog or runs the process comes from the
action's entry points: declaring both and opens the form,
alone runs it.
fcode-formsformuiTriggeruiTriggerThe submission goes through Factorial's backend, which injects ,
and server-side — declare them in
(a widget) if the process reads them. The
result follows the form conventions (, , ,
…); a multistep form follows each step's inside the same dialog.
company_idtriggered_from_locationaccess_idparametersSchema.jsonhiddenmessageformErrorsnextProcessIdnextProcessIdjson
{
"name": "Export cycle",
"factorial": {
"enabled": true,
"form": { "enabled": true },
"uiTrigger": { "enabled": true, "locationId": "compensations.cycle.header", "label": "Export…", "icon": "Download" }
}
}Files in a trigger form
A file field works, and the file does not travel through Factorial. Declare the
parameter with listing the extensions it takes:
x-fcode-filejson
"msj_file": {
"title": "FIE file",
"type": "string",
"x-fcode-file": { "accept": [".msj"] },
"ui": { "ui:widget": "file", "ui:options": { "accept": ".msj" } }
}On submit, Factorial asks the platform for an upload grant per file, PUTs the
file straight to Factorial Code storage, and sends the resulting
path as the parameter value. The process reads it as it would
any stored file ( / ).
fcode.storage://fcode-javascriptfcode-python- The grant is issued at submit, not with the schema — the platform checks
the real file name against and signs its content type, and neither is known before the user picks a file. A name outside
acceptis refused there.accept - Uploaded files are removed once they are 48 hours old. Copy anything the process needs for longer elsewhere in storage.
Labels and i18n
labelfcode.i18n("key")fcode-i18njson
"factorial": {
"enabled": true,
"uiTrigger": { "enabled": true, "locationId": "calendar.header.admin", "label": "fcode.i18n(\"acme.sync.button\")" }
}Icons
Factorial renders only these names (case-insensitive); anything else falls
back to a text-only button, and the console shows the same list as a picker:
BellCalendarChartDocumentDownloadExternalLinkGlobeGraphLinkPlayRefreshSendSettingsSparklesStarUploadHow Factorial renders them (for Factorial engineers)
The Factorial side is generic — no per-app code. In the monolith:
factorial- A location is a YAML file under a component's (
app/fcode_locations/, optionalid+resourcefor the record in scope,param_key,forward_params,result_keys,single), validated by a CI spec.legacy_uiis the reference.calendar/header_admin.yml - The page renders
from
<FactorialCodeTrigger locationId="…" params={{ cycle_id }} onSuccess onError />. It draws one button per claiming trigger (F0frontend/src/modules/factorialCode/components/FactorialCodeTriggerbutton, or the legacy design-system button foroutlinelocations), renders nothing while loading or when no installed app claims the location, and owns the form dialog. The underlying hooks (legacy_uifor the listing,useFactorialCodeTriggersfor the click) are exported for data-driven hosts such as dropdown menus.useFactorialCodeTriggerActivation - Clicks go through : it checks the action's
FactorialCode::Interactors::InvokeAction(requiredPolicies, an OR of AND groups) and a policy-scoped read of the location'sRequiredPoliciesAuthorization, forwards onlyresource, injectsforward_params/company_id/access_idover anything the browser sent, and returns the process's answer unchanged. The SPA addresses an action by its opaquetriggered_from_location; no process slug reaches the browser. Everything is gated by the Factorial Code feature flag.action_id
App developers don't touch any of this; they need the location's id and
contract from the team that owns the page.
Checklist before shipping
- Location id, forwarded params and -ness confirmed with the owning Factorial team.
single - decided explicitly —
factorial.awaitResult(the default) only for fast, user-visible outcomes;trueotherwise.false - filled in, or deliberately left empty because the button is open to anyone who can reach it.
factorial.requiredPolicies - A form-backed trigger declares the pre-filled keys it reads, and any file
parameter carries .
x-fcode-file - Tested from a dev installation in Factorial, then promoted to and released so the deploy workspaces pick it up (
prod-,fcode-release).fcode-ama