Workflow Model
How workflows run, cache results, and how to use them for wallet import and bitcoind RPC
Blockchain data retrieval is multi-step and slow by nature. A wallet import touches the address indexer, fetches transactions, resolves block headers, and computes UTXOs — each step depending on the last, each one a network call against an external service. A synchronous API that blocks on all of this doesn’t scale.
Workflows are blockd’s solution. Rather than waiting for results, your application submits a declarative list of steps and receives a workflow ID immediately. blockd executes the steps asynchronously, caches the results in RocksDB, and makes them available for repeated querying — without re-executing the underlying operations.
Execution model
Section titled “Execution model”The typical lifecycle:
POST /workflows → workflow queued, returns workflow_idGET /workflows/{id} → poll state and step progress (or subscribe to workflow.progress via WebSocket)POST /pubkeys-set/txns → query cached transactionsPOST /pubkeys-set/utxos → query cached UTXOsWorkflows move through these states: pending → running → completed, cancelled, or failed.
Terminal state workflows are retained briefly in the finished history, then expired automatically.
This caching model is intentional. Transaction history doesn’t change for confirmed transactions. Querying it repeatedly should be cheap. The workflow runs once; the REST endpoints serve the cache.
Wallet import workflows
Section titled “Wallet import workflows”A full wallet import runs these steps in sequence:
| Step | What it does |
|---|---|
pubkeys.extpubkey.import | Import derived addresses into pubkeys_set |
pubkeys.scripthash.subscribe | Subscribe addresses to the indexer |
pubkeys.scripthash.history | Retrieve transaction history from the indexer |
pubkeys.transaction.fetch | Fetch full transaction data |
blockheaders.unresolved.fetch | Resolve block headers for fetched transactions |
pubkeys.unspent.compute | Compute the UTXO set |
See the Workflow Steps for the complete step list including discrete address import steps.
Omitting steps. Steps can be omitted when not needed. Omit pubkeys.unspent.compute
if UTXOs are not required.
Refreshing after on-chain activity. Cached results reflect the chain state at the time
the workflow ran. When new transactions occur, re-submit the workflow to refresh the cache,
then query the updated results. The pubkeys.activity WebSocket subscription tells you
when to do this — it fires when new activity is detected on any tracked address in the set.
pubkeys.scripthash.subscribe must have been run for activity notifications to be active;
a workflow that omitted that step will not receive them.
core.* RPC workflows
Section titled “core.* RPC workflows”Any supported bitcoind RPC method can be called as a workflow step. This is a separate use case from wallet import — a batched RPC proxy, not a data pipeline.
{ "workflow": { "steps": [ {"step": "core.getblockcount"}, {"step": "core.estimatesmartfee", "args": [6, "ECONOMICAL"]}, {"step": "core.getblockhash", "args": [292411]} ] }}Steps execute in a single batch. Results are retrieved via
GET /workflows/{id}/results/core. Each result corresponds to a step in the submitted
workflow, in order. Individual step errors return the bitcoind error code and message
without halting the remaining steps.
In general, do not mix core.* steps with wallet import steps in the same workflow. Keep RPC
proxy workflows and pubkeys workflows separate.
For method signatures, arguments, and response formats, refer to the bitcoind RPC documentation directly — blockd passes them through unchanged. See the Core RPC Methods for the list of supported methods and permission configuration.
Progress tracking
Section titled “Progress tracking”Depending on the steps included and the volume of on-chain data being processed, workflows can range from
a few seconds to several minutes. The workflow.progress WebSocket subscription lets you track execution in
real time rather than polling blindly.
{"subscribe": "workflow.progress"}Each event reports the workflow state, current step, and percent complete within that step
— enough to drive a real-time progress indicator scoped to a specific operation. The
request_id field, if set on submission, is echoed in every progress event, making it
straightforward to correlate events back to the originating request in your application.
See the WebSocket reference for the full event schema.
Error handling
Section titled “Error handling”When a workflow fails, its state transitions to failed and it is moved to
the finished history alongside completed and cancelled workflows. The
failure is surfaced in two places:
REST — GET /workflows/{id} returns state: failed with a failed_reason
object describing the cause:
{ "id": "WTjV1t4G", "state": "failed", "failed_reason": { "reason": "indexer_error", "message": "electrum code: 4 — limit exceeded, workflow aborted, please report this" }, "elapsed_ms": 1357}WebSocket — a workflow.progress event fires with state: failed and the
same failed_reason object.
failed_reason values:
reason | Cause |
|---|---|
indexer_error | Fulcrum returned an error |
indexer_unavailable | Fulcrum connection lost during execution |
core_error | bitcoind RPC returned an error |
core_unavailable | bitcoind connection lost during execution |
internal | Unexpected internal blockd error |
All failure reasons indicate a blockd-level issue — not a misconfiguration in your workflow. The standard recovery is to resubmit the workflow. No partial results are available and no resume is possible from a failed state.
Failed workflows are retained in the finished history and expire on the same
schedule as completed workflows. A GET /workflows/{id} returning 404
after a known submission means the workflow completed and expired, or blockd
lost state — treat it as an unexpected condition, wait a backoff period, and
resubmit.