Skip to content

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.

The typical lifecycle:

POST /workflows → workflow queued, returns workflow_id
GET /workflows/{id} → poll state and step progress
(or subscribe to workflow.progress via WebSocket)
POST /pubkeys-set/txns → query cached transactions
POST /pubkeys-set/utxos → query cached UTXOs

Workflows move through these states: pendingrunningcompleted, 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.

A full wallet import runs these steps in sequence:

StepWhat it does
pubkeys.extpubkey.importImport derived addresses into pubkeys_set
pubkeys.scripthash.subscribeSubscribe addresses to the indexer
pubkeys.scripthash.historyRetrieve transaction history from the indexer
pubkeys.transaction.fetchFetch full transaction data
blockheaders.unresolved.fetchResolve block headers for fetched transactions
pubkeys.unspent.computeCompute 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.

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.

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.

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:

RESTGET /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:

reasonCause
indexer_errorFulcrum returned an error
indexer_unavailableFulcrum connection lost during execution
core_errorbitcoind RPC returned an error
core_unavailablebitcoind connection lost during execution
internalUnexpected 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.