WritingHow to Write Your First Subgraph: A Practical Step-by-Step Guide — Clixo
5 min readsubgraph, the-graph, blockchain-indexing, graphql, evm

How to Write Your First Subgraph: A Practical Step-by-Step Guide

Learn how to write a subgraph from scratch — schema design, event handlers, and deployment — with practical notes on common early mistakes to avoid.

You have deployed a smart contract and now need a way to query its event history efficiently. The Graph's subgraph system is the most established path for EVM-compatible contracts, and the tooling has matured significantly. The steep part of the learning curve is not installation — it is understanding how your schema design choices affect query performance and indexing speed downstream.

This guide walks through writing a working subgraph from start to deployment, with emphasis on the decisions that matter.

How to Write a Subgraph: The Three Core Files

Every subgraph is defined by three files:

  • subgraph.yaml — the manifest: which contract, which network, which events to index
  • schema.graphql — the entity schema: what data shape the GraphQL API will expose
  • src/mapping.ts — AssemblyScript handlers: how each event maps to entity mutations

The workflow is: emit an event on-chain → handler fires → entities are written → GraphQL query returns them.

Step 1: Install the CLI and Initialize

npm install -g @graphprotocol/graph-cli
graph init --product hosted-service my-project/my-subgraph

The init wizard asks for your contract address, network, and ABI. It generates a scaffold with all three files. Do not accept the scaffold uncritically — the generated mapping is minimal and the schema is usually not what you want for production.

Step 2: Design Your Schema First

The schema is the most important decision. Entities in schema.graphql map to database tables. Think in terms of what your UI and analytics queries will need, not what the events happen to emit.

A common mistake is creating one entity per event type. Instead, model around the domain objects your application cares about. For a DEX pair:

type Pool @entity {
  id: ID!
  token0: String!
  token1: String!
  totalVolumeUSD: BigDecimal!
  swapCount: BigInt!
}

type Swap @entity {
  id: ID!
  pool: Pool!
  amountIn: BigDecimal!
  amountOut: BigDecimal!
  timestamp: BigInt!
}

The Pool entity is updated by each Swap event. This gives you aggregate stats without running expensive GraphQL aggregation queries.

Use @entity(immutable: true) wherever possible. Immutable entities — ones written once and never updated — index significantly faster because the node skips MVCC overhead. Transaction records and raw event logs are natural candidates.

Step 3: Write the Manifest

In subgraph.yaml, specify:

  • network: the chain name (e.g., mainnet, base, arbitrum-one)
  • address: the contract address
  • abi: path to the ABI JSON file
  • startBlock: the block at which the contract was deployed

Set startBlock accurately. A subgraph that starts from block 0 will scan the full chain history before reaching your contract, adding hours or days to initial sync time.

List only the events you actually handle in eventHandlers. Every listed event triggers the indexer to fetch its receipt data, so unused events add overhead.

Step 4: Write the Handlers

Handlers in src/mapping.ts are written in AssemblyScript (a typed subset of TypeScript). Each handler receives a typed event object generated from your ABI.

import { Swap as SwapEvent } from "../generated/Pool/Pool";
import { Pool, Swap } from "../generated/schema";
 
export function handleSwap(event: SwapEvent): void {
  let pool = Pool.load(event.address.toHex());
  if (!pool) return;
 
  pool.swapCount = pool.swapCount.plus(BigInt.fromI32(1));
  pool.save();
 
  let swap = new Swap(event.transaction.hash.toHex() + "-" + event.logIndex.toString());
  swap.pool = pool.id;
  swap.amountIn = event.params.amountIn.toBigDecimal();
  swap.amountOut = event.params.amountOut.toBigDecimal();
  swap.timestamp = event.block.timestamp;
  swap.save();
}

Use the transaction hash plus log index as the entity ID for event-derived entities. This is unique by construction and avoids collision.

Step 5: Avoid eth_calls in Handlers

The single biggest performance mistake in subgraph development is calling contract.symbol() or contract.decimals() inside a handler. Each eth_call is a synchronous RPC call that blocks indexing until it returns. On historical sync across millions of blocks, this can multiply sync time by ten or more.

The correct approach: read token metadata from the contract once when the pool is created (typically in a PairCreated or equivalent initialization event handler), store it on the entity, and reference the stored value in all subsequent handlers.

Step 6: Build and Deploy

graph codegen && graph build
graph deploy --product hosted-service my-project/my-subgraph

codegen generates TypeScript types from your schema and ABI. Run it every time you change either file. build compiles the AssemblyScript to WebAssembly. deploy uploads the WASM and manifest to the indexing node.

Monitor sync progress with graph indexing-status. A fresh subgraph on a contract with significant history can take anywhere from minutes to days depending on event volume and the presence of eth_calls.

Testing Before Deployment

Use graph test with the Matchstick framework to write unit tests for your handlers. Tests mock event objects and assert entity state. This catches schema mismatches and handler logic bugs without a full sync cycle.

For integration testing, run a Graph Node locally against a Hardhat or Anvil fork, emit events via scripts, and verify the indexed output via GraphQL.

What Comes After Your First Subgraph

Once the subgraph is live, the next concerns are: query performance (add pagination, avoid deep entity nesting), reorg coverage (confirm the node's reorg depth setting matches your finality assumptions), and ongoing maintenance as your contract evolves.

Protocol upgrades, especially proxy upgrades that change event signatures, require updating both the ABI in the manifest and the handler logic — and redeploying from the correct start block.


Writing a subgraph is straightforward once the patterns click. Designing one that stays performant and maintainable across protocol iterations is a different problem. If you are building indexing infrastructure for a production protocol, Clixo can help you architect the data layer from the start.