> 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
  },
);
```

### 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 network emitting the event.
- `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
Configure block and transaction fields with [`field_selection`](/docs/HyperIndex/configuration-file#field-selection) in your `config.yaml` file.
:::

**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](./schema-file.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
    }
  },
);
```

You can also use comparison operators like `_gt`, `_gte`, `_lt`, `_lte`, and `_in` to filter entities by field value.

**Important:**

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

- Works with any field that:

  - Is used in a relationship with the [`@derivedFrom`](schema/#relationships-one-to-many-derivedfrom) directive
  - Has an [`@index`](database-performance-optimization/#creating-custom-indices-in-your-schema) directive

- 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 [Logging Guide](/docs/HyperIndex/logging).

### `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.

---
