> For the complete documentation index, see [llms.txt](https://docs.envio.dev/llms.txt).

# Event Handlers

## Registration

A handler is a function that receives blockchain data, processes it, and inserts it into the database. You can register handlers in the file defined in the `handler` field in your `config.yaml` file. By default this is `src/handlers` file.

```typescript
import { indexer } from "envio";

indexer.onEvent(
  { contract: "<CONTRACT_NAME>", event: "<EVENT_NAME>" },
  async ({ event, context }) => {
    // Your logic here
  },
);
```

:::note
The `envio` module exposes the unified `indexer` value along with types based on your `config.yaml` and `schema.graphql` files. Run **`pnpm codegen`** whenever you change these files to regenerate the types in `.envio/`.
:::

### Basic Example

Here's a handler example for the `NewGreeting` event. It belongs to the `Greeter` contract from our beginners [Greeter Tutorial](/docs/HyperIndex/greeter-tutorial):

```typescript
import { indexer, type User } from "envio";

// Handler for the NewGreeting event
indexer.onEvent(
  { contract: "Greeter", event: "NewGreeting" },
  async ({ event, context }) => {
    const userId = event.params.user; // The id for the User entity
    const latestGreeting = event.params.greeting; // The greeting string that was added
    const currentUserEntity = await context.User.get(userId); // Optional user entity that may already exist

    // Update or create a new User entity
    const userEntity: User = currentUserEntity
      ? {
          id: userId,
          latestGreeting,
          numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
          greetings: [...currentUserEntity.greetings, latestGreeting],
        }
      : {
          id: userId,
          latestGreeting,
          numberOfGreetings: 1,
          greetings: [latestGreeting],
        };

    context.User.set(userEntity); // Set the User entity in the DB
  },
);
```

### Multiple Handlers for One Event

Since v3.4, you can register as many handlers as you like for the same event, each with its own [`where`](/docs/HyperIndex/wildcard-indexing) filter. Earlier versions only accepted handlers whose filters matched.

This lets you keep broad and narrow logic in separate functions instead of branching inside one handler:

```typescript
import { indexer } from "envio";

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

// Track every ERC20 transfer on the chain
indexer.onEvent(
  { contract: "ERC20", event: "Transfer", wildcard: true },
  async ({ event, context }) => {
    // Global accounting
  },
);

// ...and run extra logic for mints, where tokens come from the zero address
indexer.onEvent(
  {
    contract: "ERC20",
    event: "Transfer",
    wildcard: true,
    where: () => ({ params: [{ from: ZERO_ADDRESS }] }),
  },
  async ({ event, context }) => {
    // Mint-specific logic
  },
);
```

An event matching more than one registration is delivered to each of them, in registration order.

### Top-Level Await

Handler files are ES modules, so you can `await` at the top level. This is useful for loading data once before registering handlers:

```typescript
import { indexer } from "envio";

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

// Load data before registering handlers
const addressesFromServer = await loadWhitelistedAddresses();

indexer.onEvent(
  {
    contract: "ERC20",
    event: "Transfer",
    wildcard: true,
    where: {
      params: [
        { from: ZERO_ADDRESS, to: addressesFromServer },
        { from: addressesFromServer, to: ZERO_ADDRESS },
      ],
    },
  },
  async ({ event, context }) => {
    // ... your handler logic
  },
);
```

### Preload Optimization

> **Important!** Preload optimization makes your handlers run **twice**.

Preload optimization is always enabled in HyperIndex V3 - there is no config flag to toggle it.

This optimization enables HyperIndex to efficiently preload entities used by handlers through batched database queries, while ensuring events are processed synchronously in their original order. When combined with the [Effect API](/docs/HyperIndex/effect-api) for external calls, this feature delivers performance improvements of multiple orders of magnitude compared to other indexing solutions.

Read more in the dedicated guides:

- [How Preload Optimization Works](/docs/HyperIndex/preload-optimization)
- [Double-Run Footgun](/docs/HyperIndex/preload-optimization#double-run-footgun)
- [Effect API](/docs/HyperIndex/event-handlers#external-calls)

### Advanced Use Cases

HyperIndex provides many features to help you build more powerful and efficient indexers. There's definitely the one for you:

- Handle Factory Contracts with [Dynamic Contract Registration](/docs/HyperIndex/dynamic-contracts) (with nested factories support)
- Perform external calls to decide which contract address to register using [Async Contract Register](/docs/HyperIndex/dynamic-contracts#async-contract-register)
- Index all ERC20 token transfers with [Wildcard Indexing](/docs/HyperIndex/wildcard-indexing)
- Use [Topic Filtering](/docs/HyperIndex/wildcard-indexing) to ignore irrelevant events
  - With [multiple filters for single event](/docs/HyperIndex/wildcard-indexing#multiple-filters)
  - With [different filters per chain](/docs/HyperIndex/wildcard-indexing#different-filters-per-chain)
  - With [filter by dynamicly registered contract addresses](/docs/HyperIndex/wildcard-indexing#index-all-erc20-transfers-to-your-contract) (eg Index all ERC20 transfers to/from your Contract)
- [Access Contract State](/docs/HyperIndex/contract-state) directly from handlers
- Perform external calls from handlers by following the [IPFS Integration guide](/docs/HyperIndex/ipfs)

---

## Event Object

Each handler receives an `event` object containing details about the emitted event, including parameters and blockchain metadata.

### Accessing Event Parameters

Event parameters are accessed via:

```typescript
event.params.<PARAMETER_NAME>
```

Example usage:

```typescript
const sender = event.params.sender;
const amount = event.params.amount;
```

### Additional Event Information

The event object also contains additional metadata:

- `event.chainId` – Chain ID of the chain emitting the event.
- `event.contractName` – Name of the contract the event belongs to, as set in `config.yaml` (eg `"ERC20"`).
- `event.eventName` – Name of the event (eg `"Transfer"`).
- `event.srcAddress` – Contract address emitting the event.
- `event.logIndex` – Index of the log within the block.
- `event.block` – Block fields (By default: `number`, `timestamp`, `hash`).
- `event.transaction` – Transaction fields (eg `hash`, `gasUsed`, etc. Empty by default).

:::tip
By default, all addresses returned in the `event` object, such as `event.transaction.from`,
`event.transaction.to`, and `event.srcAddress`, are [EIP-55](https://eips.ethereum.org/EIPS/eip-55) checksummed.
Use `.toLowerCase()` on a single value if you specifically need it in lowercase, or set
[`address_format: lowercase`](/docs/HyperIndex/configuration-file#address-format) in `config.yaml` to switch every address that the indexer surfaces (events, `chain.<Contract>.addresses`, entity ids, etc.) to lowercase globally.
:::

:::note
Which fields `event.block` and `event.transaction` carry is up to you - see [Selecting Block and Transaction Fields](#selecting-block-and-transaction-fields).
:::

### Selecting Block and Transaction Fields

Since **v3.7**, a registration names the block and transaction fields its handler reads with the `fields` option:

```typescript
indexer.onEvent(
  {
    contract: "MyContract",
    event: "Transfer",
    fields: { transaction: ["hash", "from"], block: ["timestamp"] },
  },
  async ({ event, context }) => {
    event.transaction.hash; // string
    event.block.timestamp; // number
    event.transaction.gasUsed; // Type error - not listed in fields
  },
);
```

Reading a field you didn't list is a type error, so the selection stays honest as handlers change. `event.block.number` is always available without listing it.

`fields` works on both `indexer.onEvent` and `indexer.contractRegister`, and two handlers on the same event can select different fields - the indexer only fetches and decodes what's actually asked for.

Write the selection inline, as above. To share one across registrations, declare it `as const` so its element types still name the fields:

```typescript
const txFields = { transaction: ["hash", "from"] } as const;

indexer.onEvent(
  { contract: "MyContract", event: "Transfer", fields: txFields },
  handler,
);
```

The `config.yaml` [`field_selection`](/docs/HyperIndex/configuration-file#field-selection) option still works and applies to every handler of an event (or of every event, at the root level). A handler's `fields` overrides it. Prefer `fields`: the list sits next to the code that reads it, and it doesn't make every other handler pay for the fetch.

See [`FieldSelection`](/docs/HyperIndex/config-schema-reference#def-fieldselection) in the Config Schema Reference for the full list of available transaction and block fields.

**Example event type definition:**

```typescript
type Event<Params, TransactionFields, BlockFields> = {
  params: Params;
  chainId: 1 | 137;
  srcAddress: `0x${string}`;
  logIndex: number;
  transaction: TransactionFields;
  block: BlockFields;
};
```

---

## Context Object

The handler `context` provides methods to interact with entities stored in the database.

### Retrieving Entities

Retrieve entities from the database using `context.Entity.get` where `Entity` is the name of the entity you want to retrieve, which is defined in your [schema.graphql](https://docs.envio.dev/docs/HyperIndex/schema.md) file.

```typescript
await context.Entity.get(entityId);
```

It'll return `Entity` object or `undefined` if the entity doesn't exist.

Use `context.Entity.getOrThrow` to conveniently throw an error if the entity doesn't exist:

```typescript
const pool = await context.Pool.getOrThrow(poolId);
// Will throw: Entity 'Pool' with ID '...' is expected to exist.

// Or you can pass a custom message as a second argument:
const pool = await context.Pool.getOrThrow(
  poolId,
  `Pool with ID ${poolId} is expected.`
);
```

Or use `context.Entity.getOrCreate` to automatically create an entity with default values if it doesn't exist:

```typescript
const pool = await context.Pool.getOrCreate({
  id: poolId,
  totalValueLockedETH: 0n,
});

// Which is equivalent to:
let pool = await context.Pool.get(poolId);
if (!pool) {
  pool = {
    id: poolId,
    totalValueLockedETH: 0n,
  };
  context.Pool.set(pool);
}
```

### Retrieving Entities by Field

```typescript
indexer.onEvent(
  { contract: "ERC20", event: "Approval" },
  async ({ event, context }) => {
    // Find all approvals for this specific owner
    const currentOwnerApprovals = await context.Approval.getWhere({
      owner_id: { _eq: event.params.owner },
    });

    // Process all the owner's approvals efficiently
    for (const approval of currentOwnerApprovals) {
      // Process each approval
    }
  },
);
```

Beyond `_eq`, you can filter by value using comparison operators like `_gt`, `_gte`, `_lt`, `_lte`, and `_in`.

To narrow results further, combine several conditions in a single call - they all have to match (`AND`):

```typescript
// Find accounts matching a specific id AND a balance within a range
const accounts = await context.Account.getWhere({
  id: { _eq: event.params.account },
  balance: { _gte: 1_000_000n, _lte: 10_000_000n },
});
```

:::note
Multi-field filtering with `getWhere` was added in HyperIndex v3.2.
:::

**Important:**

- Preload Optimization is always enabled in V3 and powers `getWhere`. See [How Preload Optimization Works](/docs/HyperIndex/preload-optimization).

- Works with any entity field. Since v3.5, HyperIndex creates whatever database index the query needs, so you don't have to declare [`@index`](/docs/HyperIndex/database-performance-optimization#indices-created-on-demand) for it.

- Potential Memory Issues: Very large `getWhere` queries might cause memory overflows.
- Tip: Try to put the `getWhere` query to the top of the handler, to make sure it's being preloaded. Read more about how [Preload Optimization](/docs/HyperIndex/preload-optimization) works.

### Modifying Entities

Use `context.Entity.set` to create or update an entity:

```typescript
context.Entity.set({
  id: entityId,
  ...otherEntityFields,
});
```

:::note
Both `context.Entity.set` and `context.Entity.deleteUnsafe` methods use the In-Memory Storage under the hood and don't require `await` in front of them.
:::

### Referencing Linked Entities

When your schema defines a field that links to another entity type, set the relationship using `<field>_id` with the referenced entity's `id`. You are storing the ID, not the full entity object.

```graphql
type A {
  id: ID!
  b: B!
}

type B {
  id: ID!
}
```

```typescript
context.A.set({
  id: aId,
  b_id: bId, // ID of the linked B entity
});
```

HyperIndex automatically resolves `A.b` based on the stored `b_id` when querying the API.

### Deleting Entities (Unsafe)

To delete an entity:

```typescript
context.Entity.deleteUnsafe(entityId);
```

:::warning
The `deleteUnsafe` method is experimental and **unsafe**. You need to manually handle all entity references after deletion to maintain database consistency.
:::

### Updating Specific Entity Fields

Use the following approach to update specific fields in an existing entity:

```typescript
const pool = await context.Pool.get(poolId);
if (pool) {
context.Pool.set({
  ...pool,
  totalValueLockedETH: pool.totalValueLockedETH.plus(newDeposit),
});
}
```

### `context.log`

The context object also provides a logger that you can use to log messages to the console. Compared to `console.log` calls, these logs will be displayed on our [Envio Cloud](/docs/HyperIndex/hosted-service) runtime logs page.

Read more in the [Observability Guide](/docs/HyperIndex/observability).

### `context.isPreload`

If you need to skip the preload phase for CPU-intensive operations or to perform certain actions only once per event, you can use `context.isPreload`.

```typescript
indexer.onEvent(
  { contract: "ERC20", event: "Transfer" },
  async ({ event, context }) => {
    // Load existing data efficiently
    const [sender, receiver] = await Promise.all([
      context.Account.getOrThrow(event.params.from),
      context.Account.getOrThrow(event.params.to),
    ]);

    // Skip expensive operations during preload
    if (context.isPreload) {
      return;
    }

    // CPU-intensive calculations only happen once
    const complexCalculation = performExpensiveOperation(event.params.value); // Placeholder function for demonstration

    // Create or update sender account
    context.Account.set({
      id: event.params.from,
      balance: sender.balance - event.params.value,
      computedValue: complexCalculation,
    });
    // Create or update receiver account
    context.Account.set({
      id: event.params.to,
      balance: receiver.balance + event.params.value,
    });
  },
);
```

**Note:** While `context.isPreload` can be useful for bypassing double execution, it's recommended to use the [Effect API](/docs/HyperIndex/effect-api) for external calls instead, as it provides automatic batching and memoization benefits.

## External Calls

Envio indexer runs using Node.js runtime. This means that you can use `fetch` or any other library like `viem` to perform external calls from your handlers.

Note that with [Preload Optimization](/docs/HyperIndex/preload-optimization) all handlers run twice. But with [Effect API](/docs/HyperIndex/effect-api) this behavior makes your external calls run in parallel, while keeping the processing data consistent.

Check out our [IPFS Integration](/docs/HyperIndex/ipfs), [Accessing Contract State](/docs/HyperIndex/contract-state) and [Effect API](/docs/HyperIndex/effect-api) guides for more information.

### `context.effect`

Define an effect and use it in your handler with `context.effect`:

```typescript
import { indexer, createEffect, S } from "envio";

// Define an effect that will be called from the handler.
const getMetadata = createEffect(
  {
    name: "getMetadata",
    input: S.string,
    output: {
      description: S.string,
      value: S.bigint,
    },
    rateLimit: {
      calls: 5,
      per: "second",
    },
    cache: true, // Optionally persist the results in the database
  },
  ({ input }) => {
    const response = await fetch(`https://api.example.com/metadata/${input}`);
    const data = await response.json();
    return {
      description: data.description,
      value: data.value,
    };
  }
);

indexer.onEvent(
  { contract: "ERC20", event: "Transfer" },
  async ({ event, context }) => {
    // Load metadata for the token.
    // This will be executed in parallel for all events in the batch.
    // The call is automatically memoized, so you don't need to worry about duplicate requests.
    const sender = await context.effect(getMetadata, event.params.from);

    // Process the transfer with the pre-loaded data
  },
);
```

---

## Accessing `config.yaml` Data in Handlers

You can read your indexer configuration and live indexing state from the `indexer` value - either at the top level of a handler file or inside a handler. Use `indexer.chains[chainId]` (or one of the named entries on `indexer.chains`) to inspect a specific chain:

```typescript
import { indexer } from "envio";

indexer.onEvent(
  { contract: "Greeter", event: "NewGreeting" },
  async ({ event, context }) => {
    const chain = indexer.chains[event.chainId];
    chain.id;                  // chain id
    chain.startBlock;          // configured start block
    chain.endBlock;            // configured end block (or undefined)
    chain.isRealtime;          // true once this chain has reached the head
    chain.Greeter.name;        // contract name
    chain.Greeter.abi;         // parsed ABI
    chain.Greeter.addresses;   // initial + dynamically registered addresses
  },
);
```

Top-level fields like `indexer.name`, `indexer.description`, and `indexer.chainIds` are also available. After restart, addresses on `chain.<Contract>.addresses` include any contracts that were dynamically registered in previous runs, not just those declared in `config.yaml`.

---

## Performance Considerations

For performance optimization and best practices, refer to:

- [Benchmarking](/docs/HyperIndex/benchmarking)
- [Preload Optimization](/docs/HyperIndex/preload-optimization)

These guides offer detailed recommendations on optimizing entity loading and indexing performance.

---
