--- name: fcode-python description: Write Python 3.13 for Factorial Code processes and modules — the main() entry point, fcode.context.parameters, fcode.import_module(), datastore/storage/env helpers, PEP 8 / snake_case, auto-installed dependencies, and return-value formats. Use when creating or editing .py process or module code for Factorial Code (fcode). license: MIT metadata: category: factorial-code --- # 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 `fcode-core-concepts`. | Aspect | Guideline | |---|---| | Runtime | Python 3.13 | | Process entry file | `main.py` | | Entry point | `def main()` | | Parameters | `fcode.context.parameters` | | Variables | `os.getenv("X")` or `fcode.env.X` | | Import a module | `fcode.import_module("module-slug")` | ## Gotchas - **Define `main()`** as the entry point, but **never call it yourself** — Factorial Code invokes it. - **`fcode.import_module()` names must be hardcoded string literals**, never variables: `fcode.import_module("shopify-client")` ✅, `fcode.import_module(name)` ❌. - **Never alias `fcode.i18n`** — call it literally (`fcode.i18n("key")` ✅, `t = fcode.i18n` ❌): an aliased call throws "i18n is disabled" at runtime. See `fcode-i18n`. - **Datastore stores only strings/numbers** — `json.dumps` objects before `set`, `json.loads` 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 snake_case**, not camelCase; follow PEP 8; add type hints where helpful. - Wrap the main flow in `try/except`, log the caught error with context via `fcode-logs` (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 `os.getenv`. ## Process template ```python def main(): parameters = fcode.context.parameters # Your code here return { "message": "Success!" } ``` ## Helpers ```python import os # 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 # Workspace (team) metadata team_slug = fcode.team.slug # Environment variables (secrets/config) api_key = os.getenv("API_KEY") # or fcode.env.API_KEY # 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 # Run another process fcode.processes.run("process-identifier", options) # 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 ``` ## 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 `print` (stdout for debug/info, stderr for warn/error), so call sites read like a plain `print`: ```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 `info`; payloads at `debug`; always log inside `except` with context before re-raising) 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). ## Dependencies External pip packages install automatically — just `import` them. When the package name differs from the import name, declare it with `@add-package`: ```python # @add-package requests import requests ``` Packages a parent workspace provides are already installed here — don't redeclare them (see `fcode-core-concepts`). ## Datastore & storage ```python import json, os # 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") # 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. # Keys (entry names) are never encrypted. fcode.datastore.set("oauth.token", access_token, True) token = fcode.datastore.get("oauth.token") # 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) # 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). 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")`. ## Variables, schedules & OAuth Read/write team variables and manage process schedules at runtime — scoped to your own team, no API token needed (like datastore/storage): ```python # 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. 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 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) fcode.schedule.delete_for_process(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 # `on_complete` when the provider redirects back -- so never build an authorization # 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). ### Inherited variables `list()`/`get()` include variables inherited from parent workspaces (model in `fcode-core-concepts`); an inherited one carries `resolving_team_slug` 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 `resolving_team_slug` if it must report what it actually removed). `fcode.env.set_env_var` / `del_env_var` behave the same way. ## Sending email Send email with the built-in `fcode.send_mail` — no SMTP setup required (model in `fcode-core-concepts`): ```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="HTML body", ) # 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. ## Return values ```python # 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": sensitive_data } # 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).