Skip to main content
A user function is a piece of Python source code. It is compiled, cached, and invoked inside the Python Executor process on the Worker’s own machine: each call invokes the entry function once, and the entry function’s return value is the output of that call. The function is responsible only for “computing”, not for “writing”.

Entry point conventions

The Executor picks the entry point among the top-level defs in the source by the following priority (resolve_entry_name in python/blockx_audit/tables.py):
  1. The last segment of functionId. For example, the entry point of the registered function token.trace_to_token_transfer is def trace_to_token_transfer.
  2. A top-level def named _, or an alias line _ = <function name>. When blockx-py packages a Python callable into source, it adds this alias line automatically.
  3. The first top-level def in source order whose name does not start with _.
The simplest way to write inline source is to define _ directly:
The function body must be self-contained: module-level constants and helper functions go inside the function body, and the modules you use are imported inside the function body.

Arguments and return values

Arguments are passed by position and come from the call’s args: The keys of the row dict are the column names of the source table, and the values are the JSON-decoded values. In blockx-py, the SOURCE_ROW constant replaces the hand-written "${<table>}". Return values must be JSON serializable:

Where functions come from

The source of registered functions is maintained by the Worker’s Function Code View: it subscribes to the system.function table in BlockDB, or periodically does a full refresh from a Redis hash (FUNCTION_CODE_REDIS_URL), and publishes a complete new snapshot on every change. Local development uses an in-memory function library (devstub).

Version pinning

  • A snapshot epoch is pinned when the task is activated; all calls and sub-calls of that task resolve functions against the same snapshot, and hot updates only affect tasks submitted afterwards.
  • Only the source digest is dispatched to the Executor. The Executor caches compiled artifacts by digest, and on a miss pulls the source back from the Worker by digest.
  • Within one task, binding the same functionId to different source, or an inline function colliding by name with a snapshot function, makes that ID fail deterministically.

What a function can do

What a function cannot do:
  • Write BlockDB directly. All writes go through the Writer Plugin the task declares.
  • Access external services bypassing the SDK. When auditing is enabled, import only gives you whitelisted modules.
  • Share state across calls. Module namespaces are isolated per task, and there is no ordering between calls.

Execution budget

  • A single call’s execution budget is CallDeadlineMs = 5000, counting only CPU and IO backend time, not queueing; exceeding it fails that call.
  • Pure CPU loops are preempted by the Executor’s SIGALRM timer, and the greenlet yields while waiting on IO.
  • The call budget and the task timeout are independent of each other; a task timeout converges the whole task to TIMED_OUT.

Static auditing

The Worker’s FUNCTION_CODE_AUDIT_MODE decides whether source goes through static auditing before dispatch: The audit only does ast.parse and does not execute code. It checks that syntax nodes, imports, and attribute accesses are within the whitelist, and does type tracking on return values and capability boundaries; the restricted namespace provides only SAFE_BUILTINS and facades of whitelisted modules. The Sync Invoker’s Precheck uses the same auditor, auditing without executing, which is suited to showing authors the violations before submission. See docs/specs/function-code-python-whitelist.md in the blockx repository for the full whitelist.

Summary

  • The entry point is looked up first by the last segment of functionId, then _, and finally the first public def.
  • Arguments are passed by position; scanning Builders use the "${<table>}" (SOURCE_ROW) placeholder to slot in the row dict; return dict / list[dict] / None.
  • Functions are read-only; all IO is intercepted through the SDK back to the Worker; writes are done by the Writer Plugin.
  • Each task pins one code snapshot; a single call has a 5-second budget; audit enforce mode fails closed.
Continue reading: