Slot Handlers
indexer.onSlot runs logic on every slot or at an interval — the Solana
equivalent of EVM block handlers. Use it for
time-series snapshots, periodic aggregations, or pulling extra data from RPC on a
schedule with the Effect API. For indexing program
activity, reach for instruction handlers
instead.
import { indexer } from "envio";
indexer.onSlot({ name: "MySlotHandler" }, async ({ slot, context }) => {
context.log.info(`Processing slot ${slot}`);
});
Slot handlers self-register — they need no entry in config.yaml beyond the
chain itself. With no where, the handler runs on every slot.
Options
indexer.onSlot(options, handler):
name(required) — unique name, used for logging, metrics, and progress tracking.where(optional) —({ chain }) => false | true | { slot: { _gte?, _lte?, _every? } }. Evaluated once per chain at registration to decide which chains the handler runs on and over which slot range/interval.
indexer.onSlot(
{
name: "SlotSampler",
where: ({ chain }) =>
chain.id === 0
? {
slot: {
_gte: 385_453_000, // start slot (inclusive)
_lte: 385_500_000, // end slot (inclusive)
_every: 100, // every 100th slot
},
}
: false,
},
async ({ slot, context }) => {
context.SlotPing.set({ id: slot.toString(), slot });
},
);
onBlock- The handler argument is
{ slot: number, context }— a plain slot number, not ablockobject. - The filter key is
slot(with_gte/_lte/_every), notblock.number. - There's no
intervaloption; express intervals with_every._everyaligns to_gte(or the chain start) — it fires when(slot − _gte) % _every === 0.
The handler
The handler receives { slot, context }:
slot— the slot number being processed (a plainnumber).context— entity operations (one object perschema.graphqlentity, withget/getOrThrow/getWhere/getOrCreate/set/deleteUnsafe), pluscontext.log,context.effect,context.chain(idis0for Solana), andcontext.isPreload. This is the same context as EVM handlers — see the Event Handlers context.
Enriching with RPC data via Effects
A slot number alone is rarely enough — pair onSlot with an
Effect to fetch block/transaction/account data from
RPC. Effects are deduplicated and cached, and can be rate-limited so you don't
exhaust your RPC provider. S (from envio) builds the input/output schemas
(it's the Sury library).
import { indexer, createEffect, S } from "envio";
const blockSchema = S.schema({
blockhash: S.string,
blockHeight: S.nullable(S.number),
blockTime: S.nullable(S.number),
});
const getBlock = createEffect(
{
name: "getBlock",
input: { slot: S.number },
output: S.nullable(blockSchema),
rateLimit: { calls: 3, per: "second" },
},
async ({ input }) => {
const res = await fetch(process.env.ENVIO_MAINNET_RPC_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getBlock",
params: [input.slot, { maxSupportedTransactionVersion: 1, transactionDetails: "none" }],
}),
});
const { result } = await res.json();
return result ?? null;
},
);
indexer.onSlot({ name: "BlockTracker" }, async ({ slot, context }) => {
const block = await context.effect(getBlock, { slot });
if (!block) {
context.log.info(`Slot ${slot} has no block (skipped leader)`);
return; // some slots produce no block
}
context.BlockInfo.set({
id: slot.toString(),
hash: block.blockhash,
height: block.blockHeight ?? undefined,
time: block.blockTime ? new Date(block.blockTime * 1000) : undefined,
});
});
On Solana a slot may be skipped (the leader produced no block). Handle the empty
result rather than assuming getBlock always returns data.
Preload double-run
Like all V3 handlers, slot handlers run twice (a parallel preload pass to warm the
cache, then the ordered pass). Effects are cached across both runs, so reads are
cheap, but guard non-idempotent side effects with if (context.isPreload) return;.
See Preload Optimization.
Slot handlers vs instruction handlers
| Slot handler | Instruction handler | |
|---|---|---|
| Trigger | Every slot / interval | A matched program instruction |
| Data source | RPC (via Effects) | HyperSync |
| Config needed | None (self-registers) | programs_experimental entry |
| Best for | Time-series, snapshots, scheduled pulls | Decoding protocol activity |
Reach for instruction handlers to index what programs did, and slot handlers to do something on a cadence. They compose — an instruction handler can record an event, and a slot handler can periodically roll those events into a snapshot. For raw, low-level data over large ranges, query HyperSync for Solana directly.
Related
- Instruction Handlers — the main Solana handler type.
- Effect API — external/RPC calls, caching, rate limiting.
- Configuration — chains, RPC, programs, and start slot.