Skip to main content
This page turns Task and Call and Call Builder and Writer Plugin into code. The first four examples use blockx-py (python/pyproject.toml in the blockx repository pins v0.1.72); the last section shows how to call WorkerService directly from Go without the SDK.

Prerequisites

Start the worker and examples/local_test_service/local_proxy.py as described in the Quickstart. Example 1 runs locally as-is; examples 2 to 4 read and write BlockDB tables, so point BLOCKDB_*_ADDR at a reachable BlockDB and leave STUB_BLOCKDB_DELAY_MS unset.
blockx-py only connects through the UDS proxy specified by PROXY_SOCKET_PATH. If the variable is not set, it raises RuntimeError immediately rather than silently connecting elsewhere.

Example 1: run once and get the result back

InputsCallConfig + ReturnValueHandler. This is examples/local_test_service/submit.py:
Key points:
  • LocalTestService is a sample capability backend built into the worker; no BlockDB is needed locally. Any self-contained pure-computation function works just as well.
  • Results are collected in the completion order of the individual calls and are not guaranteed to match the row order of callList. When you need to map results back to inputs, have the return value carry the input.
  • A task with ReturnValueHandler is routed to the block cluster by the SDK, because the bundle worker does not register this handler.

Example 2: compute per block and write a block table

BlockTableCallConfig + BlockTableWriteHandler(block=...). Subscribe to new blocks of the source table and submit one task per block:
Key points:
  • When you pass a callable, the SDK captures the function body with inspect.getsource and appends the line _ = trace_to_token_transfer; you can also pass Function(source_code=...) directly, or a registered function ID string.
  • params determines the function’s positional arguments: here chain_id="eth", and record is one row of the source table as a dict.
  • BlockTableWriteHandler projects only the columns of the returned dict that belong to the target table; written_rows is the number of rows written this time.
  • You can add "operator": filter(...) to triggerSources to filter and deduplicate in the Builder phase first and reduce the number of calls.

Example 3: write a normal table

Swap the handler in example 2 for NormalTableWriteHandler and the target changes from a block table to a normal table (L1). It does not need block:
condition.column cannot be the primary key id; an invalid value raises ValueError at construction time. This handler carries no block context, so the same instance can be handed directly to the backfill in example 4.

Example 4: backfill per bundle

Historical backfill submits one task per bundle (a segment of 1000 blocks): BlockBundleCallConfig reads the parquet of the bundle partition, and BlockTableWriteHandler(block_bundle=...) overwrites the same bundle of the target table. backfill_block_bundles instantiates and submits bundle by bundle over a range for you:
To send a single bundle, construct the config and handler with the number directly:
The shape of BlockTableWriteHandler must follow the cluster: the bundle cluster needs block_bundle=, the block cluster needs block=. If you send the wrong shape, the server only rejects it in the Writer phase after all calls have run, so the SDK validates before submit() and raises ValueError.
For day-to-day use, prefer blockx-py’s higher-level Pipeline: one set of triggers + target_table, backfill(block_start, block_end) for historical backfill, and update() to fill gaps first and then switch to a real-time subscription. Bundle conversion, cluster routing, slot backoff, and multi-table alignment are all handled for you:

Reading the result

task.submit() returns a TaskResult: Decide whether to resubmit based on retryable; do not infer it from the error text:
Task.task_id is a fresh uuid on every build; log it when troubleshooting, since the worker’s task finished log entries are searchable by it.

Without the SDK: call gRPC directly

Go-side contributors often need to submit tasks directly to a Worker in tests or tools. The protocol is WorkerService in api/grpc/worker/worker.proto, with Go bindings in api/grpc/worker/workerpb. The program below goes through the three steps RequestTaskSlot → SubmitTask → WatchTasks against a local worker and submits the same task as example 1:
  • RequestTaskSlot can be omitted: when SubmitTask carries no slot_id, the Worker requests a slot inline and returns ResourceExhausted when there is no capacity.
  • When going through the Coordinator, first call coordinator.v1.CoordinatorService.ReserveWorkerSlot(task_id) (api/grpc/coordinator/coordinator.proto), then open a connection to the returned worker_addr and submit with slot_id.
  • Repeating SubmitTask with the same task_id is idempotent: it returns RUNNING while the task is still running, and the terminal state while the result is still within the retention window.
  • In tests you can use the RequestTaskSlot / SubmitTask / PollUntilTerminal helpers from internal/testutil directly; they wrap exactly the calls above.

Common errors

  • Quickstart: the shortest path to a local worker and proxy.
  • Task and Call: TaskInput fields, states, and failure codes.
  • Call Builder and Writer Plugin: the wire shape of every config.
  • Protocols and interfaces: the complete fields of WorkerService / CoordinatorService.
  • Testing: submission helpers in the process / system E2E tests.
  • docs/api_reference.md in the blockx-py repository: all SDK parameters and the behavior details of Pipeline.