fcode-ui-triggers
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFactorial Code — UI triggers
Factorial Code — UI 触发器
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-cliUI 触发器可将 marketplace 应用的流程暴露为 Factorial 内的一个按钮(位于页面页眉、操作下拉菜单等处)。Factorial 页面会声明 location;已安装应用的流程可声明一个 location,Factorial 则为每个声明该 location 的流程渲染一个按钮。点击按钮会运行流程,或者打开其表单。它也是 Factorial Action 的一种入口:相关设置位于流程 的 子块中,而 位于 块顶层(整个块、旧版镜像以及 Factorial One / 后端入口点均见 ;字段参考见 )。
metadata.jsonfactorial.uiTriggerawaitResultfactorialfcode-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.
- 只有已安装的 marketplace 应用才会显示按钮。 Factorial 会列出公司已安装应用的触发器,列表读取自这些应用的 deploy workspace。普通 workspace 中的流程永远不会显示;触发器会与应用的其他部分通过相同路径交付给客户:dev → prod → 通过 release 进行 deploy。
- location id 必须来自拥有该页面的 Factorial 团队。 Location 是在 Factorial 代码中声明的(例如 ),而不是在平台上声明。平台接受任何非空
calendar.header.admin(≤ 200 个字符)——未知 location 会被静默地从列表中移除,因此拼写错误意味着“没有按钮”,而不是报错。locationId - 决定契约的是 location,而不是流程:哪些上下文参数会转发给流程、同步结果的哪些键可以到达页面,以及该 location 是否为 (同一时间只能有一个已安装应用)。索取 location id 时,也应一并确认这三项。
single - 位于
awaitResult块顶层,默认值为factorial(同步)。 旧版true默认为 fire-and-forget;镜像会将新值复制到旧字段中——因此,移入uiTrigger.awaitResult的按钮默认会同步运行,除非显式写入factorial。仅当任务执行速度快(< 50 秒)且用户必须看到结果时,才应保持"awaitResult": false;设为true时,点击操作会排队执行,用户只会看到“操作已启动”,返回值也不会显示。false - 启用表单的流程会打开表单,而不是直接运行。 开启 (或旧版
factorial.form.enabled)后,按钮会在 Factorial 内的对话框中打开表单,并且form.enabled会被忽略。awaitResult - 通过触发器打开的表单会经过 Factorial,而不是直接提交给平台。 对话框读取 schema,并通过 Factorial 自己的后端提交;该后端会携带点击用户的身份。表单自身的 仍负责控制通过公开 URL 访问表单的人(
authMode),但它不控制该对话框。fcode-forms - 无论走哪条路径,、
company_id和triggered_from_location都可信。 Factorial 会在每次调用时通过服务端注入这三个值,无论是否使用表单;location 转发的参数会覆盖浏览器发送的任何同名字段。基于这些字段进行授权是安全的。(旧版表单路径并非如此;它会将这些字段作为客户端可编辑的预填字段传入。)access_id - 谁可以点击由 决定,并且会被强制执行。 在运行任何操作之前,Factorial 会针对点击用户检查
requiredPolicies(factorial.requiredPolicies),此外还会对 location 的fcode-factorial-actions执行受策略范围限制的读取。未列出任何策略的 Action,对任何能够访问按钮的人都是开放的。resource - 未捕获的错误只显示通用消息。 抛出的异常或崩溃会以“The action could not be completed”呈现给用户,不含任何细节。需要让用户阅读的信息请返回 。
{ errors: [...] } - 更改最多需要五分钟才会显示——Factorial 会在浏览器中缓存触发器列表。
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.
singlesingle在 中进行声明,然后执行 (或在控制台的流程页面中操作 Trigger from Factorial 区域):
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"
}
}
}| 字段 | 含义 |
|---|---|
| 整个 Action 的总开关;按钮需要启用它 |
| 默认为 |
| 启用按钮。设为 |
| 按钮渲染所在的 Factorial location,由页面所属团队提供 |
| 按钮文本。可使用纯文本,或 |
| 允许列表中的一个名称(见下文);省略则创建纯文本按钮 |
旧版顶层 块(、、、、)仍支持往返读写,并会与 保持镜像同步——旧文件仍可正常工作,而且 仍会为每个流程写入 。新开发应写入 ;两者同时存在时, 会发送 (镜像规则见 )。
uiTriggerenabledlocationIdlabeliconawaitResultfactorial.uiTriggerfcode pull"uiTrigger": { "enabled": false }factorialfcode pushfactorialfcode-factorial-actions一个应用可以在同一 location 声明多个触发器——每个触发器都会渲染自己的按钮——但 Factorial 中标记为 的 location 只允许一个已安装应用:安装第二个声明该 location 的应用时,会因与第一个应用冲突而失败。不要让需要与其他应用共存的应用声明 location。
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");点击会运行流程,并将 设为:
fcode.context.parameters- location 的转发上下文参数——例如页面所显示记录的 id()——键名完全遵循 location 的允许列表;
cycle_id - ——点击用户所属的 Factorial 公司(可信:通过共享密钥注入,并覆盖浏览器发送的任何同名字段);
company_id - ——location id(可信,注入方式同上);
triggered_from_location - ——Factorial access,即点击用户在该公司的成员身份,以字符串形式提供(可信,注入方式同上)。
access_id
无可用上下文的位置(例如没有范围内记录的头部按钮)不会转发任何参数——只有上述三个可信键。点击用户的 locale 会作为执行 locale 传入,因此流程中的 会使用用户的语言()。
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-python当 时,Factorial 会等待执行完成,并将你返回的任何内容原样交给页面。无需使用外层 envelope 包装,也不会在传输过程中过滤任何内容。
awaitResult: truejavascript
// Success — the whole object reaches the page's onSuccess handler.
return { synced: 42, report_url: "https://acme.example/reports/7" };- 成功状态始终表示成功,无论响应体内容如何。返回带 键的对象并不会使其成为失败。
errors - 要表示预期内的失败,应使用错误状态,而不是响应体。 抛出 ,或从 HTTP 形态的流程返回错误状态;响应体会原样传递,因此页面可以读取你的消息。
fcode.error(...) - 未带消息的异常,或超过同步执行时限(约 50 秒;执行仍可能在后台完成)→ 用户无法据此采取行动的通用错误。
返回结果应匹配入口点的预期,这由流程作者负责:表单按照自己的约定读取结果(、、),而普通按钮只显示 toast,因此同时以两种方式暴露的流程无法获得一种可移植的统一结果结构。location 上的 仅作说明——它描述页面打算读取的内容,不会据此过滤任何内容。
messageformErrorsnextProcessIdresult_keys关闭 后,返回值只能在执行日志中查看;耗时较长的任务应放在那里执行,或者由快速同步触发器通过 启动(参见 / )。
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" }
}
}像往常一样启用表单()后,按钮就会在 Factorial 内的对话框中打开它,使用 f0 主题渲染,并预先填入转发的参数。点击后是打开对话框还是运行流程,取决于 Action 的入口点配置:同时声明 和 会打开表单,仅声明 则运行流程。
fcode-formsformuiTriggeruiTrigger提交会经过 Factorial 的后端,该后端会在服务端注入 、 和 ——如果流程会读取这些参数,应在 中声明它们(使用 widget)。结果遵循表单约定(、、 等);多步骤表单会在同一对话框中按照每一步的 继续执行。
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.
文件字段可以正常使用,而且文件不会经由 Factorial 传输。使用 声明参数及其允许的文件扩展名:
x-fcode-filejson
"msj_file": {
"title": "FIE file",
"type": "string",
"x-fcode-file": { "accept": [".msj"] },
"ui": { "ui:widget": "file", "ui:options": { "accept": ".msj" } }
}提交时,Factorial 会为每个文件向平台申请上传 grant,将文件直接 PUT 到 Factorial Code storage,并把生成的 路径作为参数值发送。流程可以像读取任何已存储文件一样读取它( / )。
fcode.storage://fcode-javascriptfcode-python- Grant 是在提交时签发的,而不是随 schema 一起签发——平台会根据真实文件名检查 并为其 content type 签名,而这两者在用户选择文件之前都未知。不符合
accept的文件名会在该阶段被拒绝。accept - 上传的文件满 48 小时后会被删除。如果流程需要更长时间保存相关内容,请将其复制到其他 storage 位置。
Labels and i18n
标签与 i18n
labelfcode.i18n("key")fcode-i18njson
"factorial": {
"enabled": true,
"uiTrigger": { "enabled": true, "locationId": "calendar.header.admin", "label": "fcode.i18n(\"acme.sync.button\")" }
}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:
BellCalendarChartDocumentDownloadExternalLinkGlobeGraphLinkPlayRefreshSendSettingsSparklesStarUploadFactorial 仅渲染以下名称(不区分大小写);其他名称都会回退为纯文本按钮,控制台也会以选择器形式显示同一列表:
BellCalendarChartDocumentDownloadExternalLinkGlobeGraphLinkPlayRefreshSendSettingsSparklesStarUploadHow Factorial renders them (for Factorial engineers)
Factorial 的渲染方式(面向 Factorial 工程师)
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.
Factorial 侧采用通用实现——不需要为每个应用编写代码。在 monolith 中:
factorial- Location 是组件 下的一个 YAML 文件(
app/fcode_locations/、用于表示范围内记录的可选id+resource,以及param_key、forward_params、result_keys、single),并由 CI spec 校验。legacy_ui是参考实现。calendar/header_admin.yml - 页面从 渲染
frontend/src/modules/factorialCode/components/FactorialCodeTrigger。它会为每个声明该 location 的触发器渲染一个按钮(使用 F0<FactorialCodeTrigger locationId="…" params={{ cycle_id }} onSuccess onError />按钮;对于outlinelocation,则使用旧版 design-system 按钮),在加载期间或没有已安装应用声明该 location 时不渲染任何内容,并负责管理表单对话框。底层 hooks(用于获取列表的legacy_ui,用于处理点击的useFactorialCodeTriggers)会导出给下拉菜单等数据驱动型宿主使用。useFactorialCodeTriggerActivation - 点击会通过 :它会检查 Action 的
FactorialCode::Interactors::InvokeAction(requiredPolicies,由 AND 组构成的 OR)以及 location 的RequiredPoliciesAuthorization是否允许在当前策略范围内读取,只转发resource,用forward_params/company_id/access_id覆盖浏览器发送的任何同名字段,并原样返回流程的响应。SPA 使用不透明的triggered_from_location寻址 Action;浏览器不会收到任何流程 slug。所有行为均受 Factorial Code feature flag 控制。action_id
应用开发者无需接触这些实现;他们只需从拥有页面的团队获取 location id 和对应契约。
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
- 已与拥有页面的 Factorial 团队确认 location id、转发的参数以及是否为 。
single - 明确决定 ——仅当任务执行速度快且结果需要对用户可见时使用
factorial.awaitResult(默认值),否则使用true。false - 已填写,或明确保留为空,表示按钮对任何能够访问它的人开放。
factorial.requiredPolicies - 基于表单的触发器声明其读取的预填键,所有文件参数均带 。
x-fcode-file - 先在 Factorial 中通过开发安装进行测试,再提升到 并发布,使 deploy workspace 能够读取到变更(
prod-、fcode-release)。fcode-ama