fcode-javascript

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Factorial Code — JavaScript

Factorial Code — JavaScript

Guidelines for writing JavaScript that runs on Factorial Code. Runtime is Node.js v22. For the platform model (processes, modules, datastore) see
fcode-core-concepts
.
AspectGuideline
RuntimeNode.js v22
Process entry file
index.js
Entry point
async function main()
Export (required)
module.exports = { main }
Parameters
fcode.context.parameters
Variables
process.env.X
or
fcode.env.X
Import a module
fcode.import("module-slug")
在 Factorial Code 上运行 JavaScript 的编写指南。运行时为 Node.js v22。有关平台模型(process、module、datastore),请参阅
fcode-core-concepts
。
方面指南
运行时Node.js v22
Process 入口文件
index.js
入口点
async function main()
导出(必需)
module.exports = { main }
参数
fcode.context.parameters
变量
process.env.X
或
fcode.env.X
导入 module
fcode.import("module-slug")

Gotchas

易错点

  • Always export
    main
    :
    module.exports = { main }
    . Without it the process won't run.
  • Never call
    main()
    yourself
    — Factorial Code invokes it.
  • fcode.import()
    names must be hardcoded string literals
    , never variables:
    fcode.import("shopify-client")
    ✅,
    fcode.import(name)
    ❌.
  • Never alias
    fcode.i18n
    — call it literally (
    fcode.i18n("key")
    ✅,
    const t = fcode.i18n
    ❌): an aliased call throws "i18n is disabled" at runtime. See
    fcode-i18n
    .
  • Datastore stores only strings/numbers —
    JSON.stringify
    objects before
    set
    , parse after
    get
    .
  • Encrypt secrets you keep in the datastore — pass
    true
    as the third argument of
    set
    (
    fcode.datastore.set(key, token, true)
    );
    get
    decrypts transparently. Plain
    set
    stores the value in clear.
  • Use
    async/await
    for all async work; wrap the main flow in
    try/catch
    , log the caught error with context via
    fcode-logs
    (see Logging), and throw actionable errors. Use
    const
    /
    let
    , never
    var
    .
  • Never hardcode or log secrets — read them from
    process.env
    .
  • 务必导出
    main
    :
    module.exports = { main }
    。如果不导出,process 将不会运行。
  • 切勿自行调用
    main()
    ——Factorial Code 会调用它。
  • fcode.import()
    的名称必须是硬编码字符串字面量
    ,绝不能使用变量:
    fcode.import("shopify-client")
    ✅,
    fcode.import(name)
    ❌。
  • 切勿为
    fcode.i18n
    创建别名
    ——应按原名调用(
    fcode.i18n("key")
    ✅,
    const t = fcode.i18n
    ❌):通过别名调用时,运行时将抛出 "i18n is disabled"。 参见
    fcode-i18n
    。
  • Datastore 只能存储字符串/数字——在
    set
    之前对对象执行
    JSON.stringify
    , 在
    get
    之后进行解析。
  • 对保存在 datastore 中的 secrets 进行加密——将
    true
    作为
    set
    的第三个参数传入 (
    fcode.datastore.set(key, token, true)
    );
    get
    会透明地解密。普通
    set
    会以明文存储该值。
  • 对所有异步工作使用
    async/await
    ;使用
    try/catch
    包裹主要流程,通过
    fcode-logs
    记录捕获到的错误及其上下文(参见 Logging),然后抛出便于定位和处理的错误。 使用
    const
    /
    let
    ,切勿使用
    var
    。
  • 切勿硬编码或记录 secrets——应从
    process.env
    读取。

Process template

Process 模板

javascript
async function main() {
  const { parameters } = fcode.context;

  // Your code here

  return { message: "Success!" };
}

module.exports = { main };
javascript
async function main() {
  const { parameters } = fcode.context;

  // Your code here

  return { message: "Success!" };
}

module.exports = { main };

Helpers

辅助工具

javascript
// Execution / process / schedule metadata
const { id, comment } = fcode.execution;
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

// Import a Factorial Code module (hardcoded name only)
const { myFunc } = fcode.import("module-name");
const { myFunc: v1 } = fcode.import("module-name", "v1.0.0"); // pinned version tag or alias

// Run another process
await fcode.processes.run("process-identifier", options);

// Translations (workspace locales — see fcode-i18n)
const greeting = fcode.i18n("greetings.hello", { name: "Ada" }); // %{name} filled in
fcode.i18n("greetings.hello", { name: "Ada" }, { locale: "es" }); // another locale, this lookup only (value may be dynamic)
const locale = fcode.i18n.locale; // the execution's locale
javascript
// Execution / process / schedule metadata
const { id, comment } = fcode.execution;
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

// Import a Factorial Code module (hardcoded name only)
const { myFunc } = fcode.import("module-name");
const { myFunc: v1 } = fcode.import("module-name", "v1.0.0"); // pinned version tag or alias

// Run another process
await fcode.processes.run("process-identifier", options);

// Translations (workspace locales — see fcode-i18n)
const greeting = fcode.i18n("greetings.hello", { name: "Ada" }); // %{name} filled in
fcode.i18n("greetings.hello", { name: "Ada" }, { locale: "es" }); // another locale, this lookup only (value may be dynamic)
const locale = fcode.i18n.locale; // the execution's locale

Logging

日志记录

Log through the shared
fcode-logs
module — level-gated logging inherited by every workspace. It reads the
LOG_LEVEL
team variable (
debug | info | warn | error
, default
info
) and forwards to the matching
console.*
, so call sites read like bare
console
calls:
javascript
const log = fcode.import("fcode-logs"); // import once; reuse `log` everywhere, including `catch`

log.info("sync started", { processSlug }); // console.log  when LOG_LEVEL ≤ info
log.debug({ requestPayload });              // console.debug only when LOG_LEVEL=debug
log.warn("token missing — skipping");       // console.warn  when LOG_LEVEL ≤ warn
log.error("sync failed", err.message);       // console.error — always emitted
Be verbose — the logging policy (start/end, external calls and decisions at
info
; payloads at
debug
; always log inside
catch
with context before re-throwing) is in
fcode-core-concepts
§General rules. Set
LOG_LEVEL=debug
in a local or dev workspace to trace a full run; production stays at
info
. Never log secrets. Each execution keeps at most 500 log lines / 500 KB — later entries are dropped (
fcode-core-concepts
› Plans & limits).
通过共享的
fcode-logs
module 记录日志——这是所有 workspace 共享并继承的分级日志机制。 它会读取团队变量
LOG_LEVEL
(
debug | info | warn | error
,默认值为
info
),并转发到对应的
console.*
,因此调用点的写法与普通
console
调用相同:
javascript
const log = fcode.import("fcode-logs"); // import once; reuse `log` everywhere, including `catch`

log.info("sync started", { processSlug }); // console.log  when LOG_LEVEL ≤ info
log.debug({ requestPayload });              // console.debug only when LOG_LEVEL=debug
log.warn("token missing — skipping");       // console.warn  when LOG_LEVEL ≤ warn
log.error("sync failed", err.message);       // console.error — always emitted
应记录充分的信息——日志策略(开始/结束、外部调用和决策记录为
info
; payload 记录为
debug
;始终在
catch
中记录上下文,然后重新抛出)见
fcode-core-concepts
§General rules。在本地或开发 workspace 中设置
LOG_LEVEL=debug
, 以跟踪完整运行过程;生产环境保持
info
。 切勿记录 secrets。每次执行最多保留 500 条日志/500 KB——后续条目会被丢弃 (
fcode-core-concepts
› Plans & limits)。

Dependencies

依赖项

External npm packages install automatically — just
require
them. When the import name differs from the package name, declare it with
@add-package
:
javascript
// @add-package axios
const axios = require("axios");
Packages a parent workspace provides are already installed here — don't redeclare them (see
fcode-core-concepts
).
外部 npm 包会自动安装——直接
require
即可。当导入名称与包名不同时,使用
@add-package
声明:
javascript
// @add-package axios
const axios = require("axios");
父级 workspace 提供的包已在此处安装——无需重复声明 (参见
fcode-core-concepts
)。

Datastore & storage

Datastore 与存储

javascript
// Datastore (strings/numbers only)
await fcode.datastore.set("key", "value");
await fcode.datastore.set("key", JSON.stringify({ name: "John", age: 30 }));
const value = await fcode.datastore.get("key");
await fcode.datastore.del("key");
// Encrypted at rest with a key owned by the workspace — for tokens, credentials,
// personal data. The third argument must be a boolean; get() is transparent.
// Keys (entry names) are never encrypted.
await fcode.datastore.set("oauth.token", accessToken, true);
const token = await fcode.datastore.get("oauth.token");

// Storage (files)
const fs = require("node:fs");
const path = require("node:path");
const localPath = path.join(process.env.TMP_DATA_DIR, "localfile.txt");

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));
// Form file params arrive as "fcode.storage://…" references — strip the
// prefix before download; see fcode-forms.
// 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");
Local disk: write temp files under
process.env.TMP_DATA_DIR
.
javascript
// Datastore (strings/numbers only)
await fcode.datastore.set("key", "value");
await fcode.datastore.set("key", JSON.stringify({ name: "John", age: 30 }));
const value = await fcode.datastore.get("key");
await fcode.datastore.del("key");
// Encrypted at rest with a key owned by the workspace — for tokens, credentials,
// personal data. The third argument must be a boolean; get() is transparent.
// Keys (entry names) are never encrypted.
await fcode.datastore.set("oauth.token", accessToken, true);
const token = await fcode.datastore.get("oauth.token");

// Storage (files)
const fs = require("node:fs");
const path = require("node:path");
const localPath = path.join(process.env.TMP_DATA_DIR, "localfile.txt");

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));
// Form file params arrive as "fcode.storage://…" references — strip the
// prefix before download; see fcode-forms.
// 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");
**本地磁盘:**将临时文件写入
process.env.TMP_DATA_DIR
下。

Variables, schedules & OAuth

变量、计划与 OAuth

Read/write team variables and manage process schedules at runtime — scoped to your own team, no API token needed (like datastore/storage):
javascript
// Team variables (config/secrets)
// Default is SENSITIVE: fcode.variables.set(key, value) creates a sensitive
// (masked, immutable-sensitivity) variable. Pass { sensitive: false } for
// plain config values.
await fcode.variables.set("API_KEY", "secret"); // sensitive by default
await fcode.variables.set("BASE_URL", "https://api.acme.com", {
  sensitive: false, // required for non-secret config
});
const v = await fcode.variables.get("API_KEY"); // { key, value, resolvingTeamSlug, ... } or undefined
const all = await fcode.variables.list();       // includes variables inherited from parents
await fcode.variables.delete("API_KEY");        // no-op on an inherited variable

// Schedules (cron or one-off dateTime) for a process
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,
});
const current = await fcode.schedule.get(schedule.id);
await fcode.schedule.update(schedule.id, { cron: "0 0 7 * * SUN" });
await fcode.schedule.pause(schedule.id);
await fcode.schedule.resume(schedule.id);
await fcode.schedule.delete(schedule.id);
// delete every schedule for a process (pass the process UUID)
await fcode.schedule.deleteForProcess(fcode.execution.process.id);

// Start a third-party OAuth authorization. The platform holds the state, the PKCE
// verifier and the one redirect_uri registered with the provider, and invokes
// `onComplete` when the provider redirects back — so never build an authorization
// URL or a state by hand, and never expose a callback webhook.
const flow = await fcode.oauth.start({
  authorizeUrl: "https://login.example.com/authorize",
  clientId: fcode.env.PROVIDER_CLIENT_ID,
  scope: ["openid", "offline_access"],
  onComplete: "oauth-callback",     // the process that receives the code
  data: { companyId },              // carried back untouched; never a secret
  extraParams: { nonce },           // optional, provider-specific
  pkce: true,                       // default
});
flow.authorizationUrl; // hand this to the form's oauth widget
onComplete
is invoked with
code
,
codeVerifier
,
redirectUri
,
data
and
state
in
fcode.context.parameters
(or
error
/
errorDescription
when the provider refused). Replay
redirectUri
in the token exchange — providers compare it byte for byte. See
fcode-examples
references/oauth-connect.md
.
fcode.variables.set/delete
only persist server-side; they are not reflected in
fcode.env
within the same run (
fcode.env
is a snapshot taken at start).
在运行时读写团队变量并管理 process 计划——这些操作的范围仅限于你自己的团队, 且无需 API token(与 datastore/storage 类似):
javascript
// Team variables (config/secrets)
// Default is SENSITIVE: fcode.variables.set(key, value) creates a sensitive
// (masked, immutable-sensitivity) variable. Pass { sensitive: false } for
// plain config values.
await fcode.variables.set("API_KEY", "secret"); // sensitive by default
await fcode.variables.set("BASE_URL", "https://api.acme.com", {
  sensitive: false, // required for non-secret config
});
const v = await fcode.variables.get("API_KEY"); // { key, value, resolvingTeamSlug, ... } or undefined
const all = await fcode.variables.list();       // includes variables inherited from parents
await fcode.variables.delete("API_KEY");        // no-op on an inherited variable

// Schedules (cron or one-off dateTime) for a process
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,
});
const current = await fcode.schedule.get(schedule.id);
await fcode.schedule.update(schedule.id, { cron: "0 0 7 * * SUN" });
await fcode.schedule.pause(schedule.id);
await fcode.schedule.resume(schedule.id);
await fcode.schedule.delete(schedule.id);
// delete every schedule for a process (pass the process UUID)
await fcode.schedule.deleteForProcess(fcode.execution.process.id);

// Start a third-party OAuth authorization. The platform holds the state, the PKCE
// verifier and the one redirect_uri registered with the provider, and invokes
// `onComplete` when the provider redirects back — so never build an authorization
// URL or a state by hand, and never expose a callback webhook.
const flow = await fcode.oauth.start({
  authorizeUrl: "https://login.example.com/authorize",
  clientId: fcode.env.PROVIDER_CLIENT_ID,
  scope: ["openid", "offline_access"],
  onComplete: "oauth-callback",     // the process that receives the code
  data: { companyId },              // carried back untouched; never a secret
  extraParams: { nonce },           // optional, provider-specific
  pkce: true,                       // default
});
flow.authorizationUrl; // hand this to the form's oauth widget
调用
onComplete
时,
code
、
codeVerifier
、
redirectUri
、
data
和
state
会通过
fcode.context.parameters
传入;如果提供方拒绝授权,则传入
error
/
errorDescription
。在 token exchange 中原样重放
redirectUri
——提供方会逐字节比较它。 参见
fcode-examples
的
references/oauth-connect.md
。
fcode.variables.set/delete
仅在服务端持久化;同一次运行中,它们不会反映到
fcode.env
中(
fcode.env
是启动时获取的快照)。

Inherited variables

继承变量

list()
/
get()
include variables inherited from parent workspaces (model in
fcode-core-concepts
); an inherited one carries
resolvingTeamSlug
naming its owner.
set()
on an inherited key creates an override in this workspace — the only way to change the value from here — and
delete()
on one is a silent no-op, so an uninstall process never removes a parent's credential (check
resolvingTeamSlug
if it must report what it actually removed).
fcode.env.setEnvVar
/
delEnvVar
behave the same way.
list()
/
get()
包含从父级 workspace 继承的变量(模型见
fcode-core-concepts
);继承变量会携带
resolvingTeamSlug
,用于指明其所有者。 对继承的 key 执行
set()
会在当前 workspace 中创建一个覆盖值——这是从此处更改该值的 唯一方式——而对继承变量执行
delete()
则为静默无操作,因此卸载 process 绝不会删除 父级的凭据(如果必须准确报告实际删除的内容,请检查
resolvingTeamSlug
)。
fcode.env.setEnvVar
/
delEnvVar
的行为相同。

Sending email

发送电子邮件

Send email with the built-in
fcode.sendMail
— no SMTP setup required (model in
fcode-core-concepts
):
javascript
const info = await fcode.sendMail({
  to: "user@example.com", // string or string[]
  subject: "Report ready",
  text: "Plain-text body", // provide text, html, or both
  html: "<b>HTML body</b>",
});
// info => { messageId, accepted, rejected }
  • The
    From
    address is fixed by the platform; a
    from
    you pass is ignored.
  • Each execution can send up to 3 emails by default; once the limit is reached, further calls throw.
  • Locally (
    fcode run
    ) there is no manager, so the email is logged, not sent.
  • Acceptable use — who may be a recipient, what the body may carry — is a platform rule that blocks a release when broken; see
    fcode-core-concepts
    §Sending email.
使用内置的
fcode.sendMail
发送电子邮件——无需配置 SMTP(模型见
fcode-core-concepts
):
javascript
const info = await fcode.sendMail({
  to: "user@example.com", // string or string[]
  subject: "Report ready",
  text: "Plain-text body", // provide text, html, or both
  html: "<b>HTML body</b>",
});
// info => { messageId, accepted, rejected }
  • From
    地址由平台固定;传入的
    from
    会被忽略。
  • 每次执行默认最多可发送 3 封电子邮件;达到上限后,后续调用将抛出异常。
  • 在本地(
    fcode run
    )运行时不存在 manager,因此电子邮件只会被记录而不会实际发送。
  • 可接受的使用方式——哪些用户可以成为收件人、正文可以包含哪些内容——由平台规则约束; 如果违反这些规则,发布将被阻止。参见
    fcode-core-concepts
    §Sending email。

Return values

返回值

javascript
// Standard
return { message: "Success!" };

// Custom HTTP status (webhooks)
return { status: 404, body: { message: "Not found" }, headers: { "Content-Type": "application/json" } };

// Transient (not persisted in execution results)
return { transient: true, data: sensitiveData };

// Synchronous UI trigger button inside Factorial (see fcode-ui-triggers).
// Returned unchanged to the page; signal a failure with an error, not in the body.
return { synced: 42 };
The return value is capped at 500 KB — larger results fail the execution; put them in storage and return a reference (
fcode-core-concepts
› Plans & limits).
javascript
// Standard
return { message: "Success!" };

// Custom HTTP status (webhooks)
return { status: 404, body: { message: "Not found" }, headers: { "Content-Type": "application/json" } };

// Transient (not persisted in execution results)
return { transient: true, data: sensitiveData };

// Synchronous UI trigger button inside Factorial (see fcode-ui-triggers).
// Returned unchanged to the page; signal a failure with an error, not in the body.
return { synced: 42 };
返回值上限为 500 KB——更大的结果会导致执行失败;应将其放入 storage 并返回引用 (
fcode-core-concepts
› Plans & limits)。