Factorial Code — Python
Factorial Code — Python
Guidelines for writing Python that runs on Factorial Code. Runtime is
Python 3.13. For the platform model (processes, modules, datastore) see
.
| Aspect | Guideline |
|---|
| Runtime | Python 3.13 |
| Process entry file | |
| Entry point | |
| Parameters | |
| Variables | or |
| Import a module | fcode.import_module("module-slug")
|
在 Factorial Code 上运行 Python 的指南。运行时为
Python 3.13。有关平台模型(流程、模块、datastore),请参阅
。
| 方面 | 指南 |
|---|
| 运行时 | Python 3.13 |
| 流程入口文件 | |
| 入口点 | |
| 参数 | |
| 变量 | 或 |
| 导入模块 | fcode.import_module("module-slug")
|
- Define as the entry point, but never call it yourself —
Factorial Code invokes it.
- names must be hardcoded string literals, never
variables:
fcode.import_module("shopify-client")
✅,
fcode.import_module(name)
❌.
- Never alias — call it literally ( ✅,
❌): an aliased call throws "i18n is disabled" at
runtime. See .
- Datastore stores only strings/numbers — objects before
, after .
- Encrypt secrets you keep in the datastore — pass as the third
argument of (
fcode.datastore.set(key, token, True)
); decrypts
transparently. Plain stores the value in clear.
- Use snake_case, not camelCase; follow PEP 8; add type hints where helpful.
- Wrap the main flow in , log the caught error with context via
(see Logging), and raise actionable errors. Don't rely on global
variables for state — pass it through parameters or return values.
- Never hardcode or log secrets — read them from .
- 将 定义为入口点,但切勿自行调用它——
Factorial Code 会调用它。
- 中的名称必须是硬编码的字符串字面量,
绝不能使用变量:
fcode.import_module("shopify-client")
✅,
fcode.import_module(name)
❌。
- 切勿为 创建别名——应直接调用它( ✅,
❌):通过别名调用会在运行时抛出
"i18n is disabled"。请参阅 。
- Datastore 仅存储字符串/数字——在 之前使用
序列化对象,在 之后使用 反序列化。
- 加密存放在 datastore 中的机密信息——将 作为 的第三个
参数传入(
fcode.datastore.set(key, token, True)
); 会透明地解密。
普通的 会以明文存储该值。
- 使用 snake_case,不要使用 camelCase;遵循 PEP 8;并在有帮助的地方
添加类型提示。
- 使用 包裹主流程,通过 记录捕获的错误及其上下文
(参见“日志”),然后抛出便于定位和处理的错误。不要依赖全局变量保存状态——
应通过参数或返回值传递状态。
- 切勿硬编码或记录机密信息——请通过 读取。
python
def main():
parameters = fcode.context.parameters
# Your code here
return { "message": "Success!" }
python
def main():
parameters = fcode.context.parameters
# Your code here
return { "message": "Success!" }
Execution / process / schedule metadata
Execution / process / schedule metadata
execution_id = fcode.execution.id
process_id = fcode.execution.process.id
schedule_id = fcode.execution.schedule.id # when run from a schedule
timezone = fcode.execution.timezone
execution_id = fcode.execution.id
process_id = fcode.execution.process.id
schedule_id = fcode.execution.schedule.id # when run from a schedule
timezone = fcode.execution.timezone
Workspace (team) metadata
Workspace (team) metadata
team_slug = fcode.team.slug
team_slug = fcode.team.slug
Environment variables (secrets/config)
Environment variables (secrets/config)
api_key = os.getenv("API_KEY") # or fcode.env.API_KEY
api_key = os.getenv("API_KEY") # or fcode.env.API_KEY
Import a Factorial Code module (hardcoded name only)
Import a Factorial Code module (hardcoded name only)
client = fcode.import_module("module-name")
client_v1 = fcode.import_module("module-name", "v1.0.0") # pinned version tag or alias
client = fcode.import_module("module-name")
client_v1 = fcode.import_module("module-name", "v1.0.0") # pinned version tag or alias
Run another process
Run another process
fcode.processes.run("process-identifier", options)
fcode.processes.run("process-identifier", options)
Translations (workspace locales — see fcode-i18n)
Translations (workspace locales — see fcode-i18n)
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)
locale = fcode.i18n.locale # the execution's locale
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)
locale = fcode.i18n.locale # the execution's locale
Log through the shared
module — level-gated logging inherited by
every workspace. It reads the
team variable
(
debug | info | warn | error
, default
) and forwards to
(stdout
for debug/info, stderr for warn/error), so call sites read like a plain
:
python
log = fcode.import_module("fcode-logs") # import once; reuse `log` everywhere, including `except`
log.info("sync started", process_slug) # stdout when LOG_LEVEL ≤ info
log.debug(request_payload) # stdout only when LOG_LEVEL=debug
log.warn("token missing — skipping") # stderr when LOG_LEVEL ≤ warn
log.error("sync failed", str(err)) # stderr — always emitted
Be verbose — the logging policy (start/end, external calls and decisions at
; payloads at
; always log inside
with context before
re-raising) is in
§General rules. Set
in a local or dev workspace to trace a full run; production stays at
.
Never log secrets. Each execution keeps at most 500 log lines / 500 KB — later entries
are dropped (
› Plans & limits).
通过共享的
模块记录日志——它采用级别控制的日志机制,并被每个
工作区继承。它读取团队变量
(
debug | info | warn | error
,默认为
),并转发到
(
/
输出到 stdout,
/
输出到 stderr),因此调用处
看起来就像普通的
:
python
log = fcode.import_module("fcode-logs") # import once; reuse `log` everywhere, including `except`
log.info("sync started", process_slug) # stdout when LOG_LEVEL ≤ info
log.debug(request_payload) # stdout only when LOG_LEVEL=debug
log.warn("token missing — skipping") # stderr when LOG_LEVEL ≤ warn
log.error("sync failed", str(err)) # stderr — always emitted
请务必详细记录——日志策略(开始/结束、外部调用和决策使用
;
有效载荷使用
;始终在
中记录上下文,然后重新抛出)见
§General rules。在本地或开发工作区中设置
以跟踪完整运行;生产环境保持为
。
切勿记录机密信息。每次执行最多保留 500 行日志 / 500 KB——后续条目
将被丢弃(
› Plans & limits)。
External pip packages install automatically — just
them. When the
package name differs from the import name, declare it with
:
外部 pip 包会自动安装——直接
即可。当包名与导入名不同时,
使用
声明:
@add-package requests
@add-package requests
import requests
Packages a parent workspace provides are already installed here — don't
redeclare them (see `fcode-core-concepts`).
import requests
父工作区提供的包已在此安装——不要重复声明
(参见 `fcode-core-concepts`)。
Datastore & storage
Datastore 与存储
Datastore (strings/numbers only)
Datastore (strings/numbers only)
fcode.datastore.set("key", "value")
fcode.datastore.set("key", json.dumps({ "name": "John", "age": 30 }))
value = fcode.datastore.get("key")
fcode.datastore.delete("key")
fcode.datastore.set("key", "value")
fcode.datastore.set("key", json.dumps({ "name": "John", "age": 30 }))
value = fcode.datastore.get("key")
fcode.datastore.delete("key")
Encrypted at rest with a key owned by the workspace — for tokens, credentials,
Encrypted at rest with a key owned by the workspace — for tokens, credentials,
personal data. The third argument must be a bool; get() is transparent.
personal data. The third argument must be a bool; get() is transparent.
Keys (entry names) are never encrypted.
Keys (entry names) are never encrypted.
fcode.datastore.set("oauth.token", access_token, True)
token = fcode.datastore.get("oauth.token")
fcode.datastore.set("oauth.token", access_token, True)
token = fcode.datastore.get("oauth.token")
Storage (files)
Storage (files)
local_path = os.path.join(os.environ.get("TMP_DATA_DIR"), "localfile.txt")
with open(local_path, "rb") as f:
obj = fcode.storage.upload("path/myfile.txt", f)
objects = fcode.storage.list()
content = fcode.storage.download("path/myfile.txt")
with open(local_path, "wb") as f:
f.write(content)
local_path = os.path.join(os.environ.get("TMP_DATA_DIR"), "localfile.txt")
with open(local_path, "rb") as f:
obj = fcode.storage.upload("path/myfile.txt", f)
objects = fcode.storage.list()
content = fcode.storage.download("path/myfile.txt")
with open(local_path, "wb") as f:
f.write(content)
Form file params arrive as "fcode.storage://…" references — strip the
Form file params arrive as "fcode.storage://…" references — strip the
prefix before download; see fcode-forms.
prefix before download; see fcode-forms.
Signed download URL — { "url", "expiresAt" }. A real HTTPS link in the
Signed download URL — { "url", "expiresAt" }. A real HTTPS link in the
cloud, a file:// URL locally (same shape, no special-casing).
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")
**Local disk:** write temp files under `os.environ.get("TMP_DATA_DIR")`.
signed = fcode.storage.create_signed_url("path/myfile.txt")
fcode.storage.delete("path/myfile.txt")
**本地磁盘:**将临时文件写入 `os.environ.get("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):
在运行时读写团队变量并管理流程计划——这些操作的范围限定为您自己的团队,
无需 API token(与 datastore/storage 一样):
Team variables (config/secrets)
Team variables (config/secrets)
Default is SENSITIVE: fcode.variables.set(key, value) creates a sensitive
Default is SENSITIVE: fcode.variables.set(key, value) creates a sensitive
(masked, immutable-sensitivity) variable. Pass sensitive=False for plain
(masked, immutable-sensitivity) variable. Pass sensitive=False for plain
config values.
config values.
fcode.variables.set("API_KEY", "secret") # sensitive by default
fcode.variables.set("BASE_URL", "
https://api.acme.com", sensitive=False) # required for non-secret config
v = fcode.variables.get("API_KEY") # TeamVariable (has resolving_team_slug) or None
all_vars = fcode.variables.list() # includes variables inherited from parents
fcode.variables.delete("API_KEY") # no-op on an inherited variable
fcode.variables.set("API_KEY", "secret") # sensitive by default
fcode.variables.set("BASE_URL", "
https://api.acme.com", sensitive=False) # required for non-secret config
v = fcode.variables.get("API_KEY") # TeamVariable (has resolving_team_slug) or None
all_vars = fcode.variables.list() # includes variables inherited from parents
fcode.variables.delete("API_KEY") # no-op on an inherited variable
Schedules (cron or one-off date_time) for a process
Schedules (cron or one-off date_time) for a process
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)
current = fcode.schedule.get(schedule["id"])
fcode.schedule.update(schedule["id"], cron="0 0 7 * * SUN")
fcode.schedule.pause(schedule["id"])
fcode.schedule.resume(schedule["id"])
fcode.schedule.delete(schedule["id"])
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)
current = fcode.schedule.get(schedule["id"])
fcode.schedule.update(schedule["id"], cron="0 0 7 * * SUN")
fcode.schedule.pause(schedule["id"])
fcode.schedule.resume(schedule["id"])
fcode.schedule.delete(schedule["id"])
delete every schedule for a process (pass the process UUID)
delete every schedule for a process (pass the process UUID)
fcode.schedule.delete_for_process(fcode.execution.process.id)
fcode.schedule.delete_for_process(fcode.execution.process.id)
Start a third-party OAuth authorization. The platform holds the state, the PKCE
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
verifier and the one redirect_uri registered with the provider, and invokes
when the provider redirects back -- so never build an authorization
when the provider redirects back -- so never build an authorization
URL or a state by hand, and never expose a callback webhook.
URL or a state by hand, and never expose a callback webhook.
flow = fcode.oauth.start(
authorize_url="
https://login.example.com/authorize",
client_id=fcode.env.PROVIDER_CLIENT_ID,
scope=["openid", "offline_access"],
on_complete="oauth-callback", # the process that receives the code
data={"companyId": company_id}, # carried back untouched; never a secret
extra_params={"nonce": nonce}, # optional, provider-specific
pkce=True, # default
)
flow.authorization_url # hand this to the form's oauth widget
`on_complete` 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).
flow = fcode.oauth.start(
authorize_url="
https://login.example.com/authorize",
client_id=fcode.env.PROVIDER_CLIENT_ID,
scope=["openid", "offline_access"],
on_complete="oauth-callback", # the process that receives the code
data={"companyId": company_id}, # carried back untouched; never a secret
extra_params={"nonce": nonce}, # optional, provider-specific
pkce=True, # default
)
flow.authorization_url # hand this to the form's oauth widget
`on_complete` 会通过 `fcode.context.parameters` 接收 `code`、`codeVerifier`、
`redirectUri`、`data` 和 `state`(如果提供方拒绝请求,则接收 `error` /
`errorDescription`)。在令牌交换时原样回传 `redirectUri`——提供方会逐字节
进行比较。请参阅 `fcode-examples` `references/oauth-connect.md`。
`fcode.variables.set/delete` 仅在服务端持久化;它们不会反映到同一次运行内的
`fcode.env` 中(`fcode.env` 是启动时获取的快照)。
/
include variables inherited from parent workspaces (model in
); an inherited one carries
naming
its owner.
on an inherited key creates an
override in this
workspace — the only way to change the value from here — and
on one
is a
silent no-op, so an uninstall process never removes a parent's
credential (check
if it must report what it actually
removed).
/
behave the same way.
/
包含从父工作区继承的变量(模型见
);继承变量会携带
来标识其所有者。
在继承的键上执行
会在当前工作区创建一个
覆盖值——这是从这里
更改该值的唯一方式——而在继承变量上执行
是
静默无操作,
因此卸载流程绝不会移除父工作区的凭据(如果必须报告实际删除了什么,请检查
)。
/
的行为也相同。
Send email with the built-in
— no SMTP setup required (model
in
):
python
info = fcode.send_mail(
to="user@example.com", # string or list[str]
subject="Report ready",
text="Plain-text body", # provide text, html, or both
html="<b>HTML body</b>",
)
使用内置的
发送电子邮件——无需配置 SMTP(模型见
):
python
info = fcode.send_mail(
to="user@example.com", # string or list[str]
subject="Report ready",
text="Plain-text body", # provide text, html, or both
html="<b>HTML body</b>",
)
info => { "messageId", "accepted", "rejected" }
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.
- `From` 地址固定由平台决定;传入的 `from` 会被忽略。
- 每次执行默认最多可发送 3 封电子邮件;达到上限后,进一步调用会抛出异常。
- 在本地(`fcode run`)没有 manager,因此电子邮件只会被记录,不会实际发送。
- 可接受的使用方式——哪些人可以作为收件人、正文可以携带哪些内容——属于平台规则;
当违反这些规则时,会阻止发布;参见 `fcode-core-concepts`
§Sending email。
return { "message": "Success!" }
return { "message": "Success!" }
Custom HTTP status (webhooks)
Custom HTTP status (webhooks)
return { "status": 404, "body": { "message": "Not found" }, "headers": { "Content-Type": "application/json" } }
return { "status": 404, "body": { "message": "Not found" }, "headers": { "Content-Type": "application/json" } }
Transient (not persisted in execution results)
Transient (not persisted in execution results)
return { "transient": True, "data": sensitive_data }
return { "transient": True, "data": sensitive_data }
Synchronous UI trigger button inside Factorial (see fcode-ui-triggers).
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.
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).
return { "synced": 42 }
返回值上限为 500 KB——更大的结果会导致执行失败;请将其放入 storage 并返回引用
(`fcode-core-concepts` › Plans & limits)。