> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockx.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Function code

> What a user function looks like in BlockX: entry point conventions, arguments and return values, the inline and registered sources, the code version pinned per task, the SDKs available inside a function, and static auditing.

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 `def`s 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:

```python theme={null}
def _(chain_id, record):
    value = float(record.get("value") or 0)
    if value <= 0 or not record.get("tx_id"):
        return None
    return {
        "id": record["id"],
        "chain_id": chain_id,
        "from_addr": record["from_addr"],
        "to_addr": record["to_addr"],
        "value": value,
    }
```

The function body must be self-contained: module-level constants and helper functions go inside the function body, and the modules you use are `import`ed inside the function body.

## Arguments and return values

Arguments are passed by position and come from the call's `args`:

| Call Builder                                     | Where the arguments come from                                                                                                                                                                                                                  |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InputsCallConfig`                               | Each row of `callList` is the argument array of one invocation                                                                                                                                                                                 |
| `BlockTableCallConfig` / `BlockBundleCallConfig` | Assembled from the `params` template: the position holding `"${<table>}"` is replaced with one row of the trigger table (a dict), and the other positions are passed through as is; when `params` is empty, the whole row is the only argument |

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:

| Return value | Meaning                                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------------------------- |
| `dict`       | One row. Table-writing plugins project only the columns that belong to the target table; extra keys are ignored |
| `list[dict]` | Multiple rows                                                                                                   |
| `None`       | This invocation produces nothing. Table-writing plugins skip it; `ReturnValueHandler` records it as `null`      |

## Where functions come from

| Source              | How it is declared                                                                                                    | Identity                                                                                   |
| ------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Inline source       | Submitted with the task: `code.sourceCode`; in blockx-py, use `Function(source_code=...)` or pass a callable directly | The Worker generates `inline-<first 8 chars of the digest>` from the SHA-256 of the source |
| Registered function | Only `functionId` is given (in the form `space.name`); the source comes from the function table                       | `functionId` plus the digest in the snapshot                                               |

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

| Capability           | How to write it                                                     | What happens                                                                                                                                                                                     |
| -------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Read BlockDB         | `Table` / `BlockTable` / `TimeTable` from blockdb-py                | The gRPC channel is replaced with a BridgeChannel when the Executor starts; requests go back over UDS to the Worker's IO subsystem and benefit from caching, singleflight, and admission control |
| Call a chain node    | leafage-py (`ChainState` and others)                                | Likewise patched back to the Worker's NodeRPC adapter                                                                                                                                            |
| Call other functions | `from blockx import function`, `function.call("space.name", *args)` | Becomes a sub-call inside the Executor: same task tree, same code snapshot, same IO scope; depth limit `MaxSubcallDepth = 8`                                                                     |
| Capability backends  | Classes such as `LocalTestService` and `Router` in blockx-py        | Ordinary gRPC clients; intercepted and dispatched inside the Worker process                                                                                                                      |
| `time.sleep`         | stdlib                                                              | Patched into a local timer that yields the greenlet, without occupying the Executor                                                                                                              |

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:

| Mode            | Behavior                                                                                                                                                                                |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `off` (default) | No auditing; the function runs in an ordinary namespace                                                                                                                                 |
| `enforce`       | A failed audit or an unavailable auditor both refuse dispatch; the call fails with `function_code_audit_rejected` and is not retryable; code that passes runs in a restricted namespace |
| `dark`          | Audits and logs, but does not block                                                                                                                                                     |

The audit only does `ast.parse` and does not execute code. It checks that syntax nodes, `import`s, 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:

* [Function Code View](/en/components/function-code): the implementation of snapshots, epochs, the syncer, and the audit chain.
* [Python Executor](/en/components/python-executor): the user function programming model, greenlet suspension, and SDK interception points.
* [Call Builder and Writer Plugin](/en/concepts/builders-and-handlers): how the `params` template and return values land in tables.
* [Submit a task: examples](/en/development/submit-task-example): a complete runnable function and task.
