# How the Gatsby Build Process Works: A Deep Dive into createPages, sourceNodes, and onCreateNode

> Explore the Gatsby build process. Discover how sourceNodes, onCreateNode, and createPages APIs fetch, enrich, and route data to build static pages.

- Repository: [Gatsby/gatsby](https://github.com/gatsbyjs/gatsby)
- Tags: deep-dive
- Published: 2026-03-06

---

**During the Gatsby build process, the framework executes a deterministic sequence of lifecycle APIs—`sourceNodes` to fetch data, `onCreateNode` to enrich nodes, and `createPages` to generate routes—coordinated by the [`api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/api-runner-node.js) orchestrator to produce static HTML and JavaScript bundles.**

The Gatsby build pipeline transforms raw content into optimized static assets through a plugin-driven architecture. According to the `gatsbyjs/gatsby` source code, this process relies on a central runner that invokes specific APIs at precise phases, ensuring data is sourced, transformed, and ultimately rendered into pages. Understanding these three core APIs is essential for customizing how Gatsby handles content from filesystems, CMSs, or external APIs.

## The Seven Phases of the Gatsby Build Pipeline

When you execute `gatsby build` or `gatsby develop`, Gatsby progresses through distinct stages defined in `packages/gatsby/src/bootstrap/` and [`packages/gatsby/src/utils/api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/api-runner-node.js):

1. **Bootstrap** — Loads [`gatsby-config.js`](https://github.com/gatsbyjs/gatsby/blob/main/gatsby-config.js), resolves plugins, and initializes the Redux store.
2. **Source & Transform** — Executes `sourceNodes` across all plugins to create raw data nodes.
3. **Node Enrichment** — Runs `onCreateNode` for every created node to add derived fields or child nodes.
4. **Schema Generation** — Builds the GraphQL schema from all node types in [`packages/gatsby/src/schema/schema.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/schema/schema.js).
5. **Page Creation** — Invokes `createPages` and `createPagesStatefully` so plugins can declare routes using `actions.createPage`.
6. **HTML Rendering** — Renders React trees to static HTML during the `build-html` stage (see `case \`build-html\`` at line 151 of [`packages/gatsby/src/utils/webpack.config.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/webpack.config.js)).
7. **JS Bundling** — Compiles client-side bundles during the `build-javascript` stage (line 164 of the same webpack config).

The [`api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/api-runner-node.js) file serves as the execution engine, binding plugin actions, injecting tracing spans, and managing the lifecycle transitions between these phases.

## How sourceNodes Creates the Data Graph

The **`sourceNodes`** API is the entry point for populating Gatsby's internal data layer. Plugins implement this async function to fetch remote or local data and create nodes using `actions.createNode`.

In [`packages/gatsby/src/utils/api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/api-runner-node.js), the runner invokes `sourceNodes` at lines 528–538, creating a bound actions object for each plugin. Here is a typical implementation pattern:

```javascript
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
  const data = await fetchRemoteData();  // Async I/O operation
  
  const node = {
    id: createNodeId(`my-data-${data.id}`),
    parent: null,
    children: [],
    internal: {
      type: `MyData`,
      contentDigest: createContentDigest(data),
    },
    ...data,
  };
  
  actions.createNode(node);  // Registers node in the Redux store
};

```

Nodes created during this phase become available in the GraphQL schema. The runner specifically checks for `sourceNodes` at line 528 and optionally defers certain actions to optimize performance during bulk node creation.

## Enriching Nodes with onCreateNode

After `sourceNodes` creates a node, Gatsby immediately runs **`onCreateNode`** for every plugin that exports it. This synchronous API allows transformation of nodes before the schema is generated.

The implementation in [`api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/api-runner-node.js) (lines 566–640) detects this API and wraps it with node-mutation tracking (enabled via `shouldDetectNodeMutations` at lines 437–444). Common use cases include:

- **Adding custom fields** using `actions.createNodeField`
- **Creating child nodes** (e.g., transforming Markdown into `MarkdownRemark` nodes)
- **Modifying internal metadata** like node types

```javascript
exports.onCreateNode = ({ node, actions, getNode }) => {
  if (node.internal.type === `MarkdownRemark`) {
    const fileNode = getNode(node.parent);
    
    actions.createNodeField({
      node,
      name: `slug`,
      value: `/blog/${fileNode.name}/`,
    });
  }
};

```

This two-step approach—`sourceNodes` for async I/O and `onCreateNode` for synchronous enrichment—ensures all derived data exists before GraphQL schema compilation begins.

## Generating Pages with createPages and createPagesStatefully

Once the schema is ready, Gatsby executes page creation APIs to transform nodes into routable pages.

### createPages

The standard **`createPages`** API receives `graphql` and `actions` objects, allowing plugins to query the node graph and call `actions.createPage` for each route. The runner implements special safeguards in [`api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/api-runner-node.js) (lines 81–94), wrapping `createPage` to warn if called after the API promise resolves:

```javascript
exports.createPages = async ({ graphql, actions, reporter }) => {
  const { data } = await graphql(`
    {
      allMarkdownRemark {
        nodes {
          id
          fields { slug }
        }
      }
    }
  `);
  
  data.allMarkdownRemark.nodes.forEach(node => {
    actions.createPage({
      path: node.fields.slug,
      component: require.resolve(`./src/templates/blog-post.js`),
      context: { id: node.id },  // Passed as props to the page component
    });
  });
};

```

The wrapper tracks `apiFinished` (line 100) to prevent asynchronous race conditions where pages might be created after the phase officially ends.

### createPagesStatefully

Plugins needing to create pages *after* the initial creation phase export **`createPagesStatefully`**. This API receives the Redux `store` and is invoked once the page list is fully populated.

The internal dev-404-page plugin ([`packages/gatsby/src/internal-plugins/dev-404-page/gatsby-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/internal-plugins/dev-404-page/gatsby-node.js), line 5) demonstrates this pattern:

```javascript
exports.createPagesStatefully = async ({ store, actions }) => {
  const { createPage } = actions;
  
  createPage({
    path: `/404.html`,
    component: require.resolve(`./src/pages/404.js`),
    matchPath: `/*`,  // Catch-all pattern
  });
};

```

Gatsby triggers this specifically via [`public.js`](https://github.com/gatsbyjs/gatsby/blob/main/public.js) (line 421) using the traceId `initial-createPagesStatefully`, ensuring it runs after normal `createPages` completes.

## The api-runner-node.js Orchestration Layer

All these APIs converge in [`packages/gatsby/src/utils/api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/api-runner-node.js), which implements the `apiRunnerNode` function (line 16). For each lifecycle API, the runner:

- Creates a **tracing span** (`pluginSpan`) for performance monitoring
- Binds plugin-specific action creators using `bindActionCreators`
- Constructs a **rich context object** (lines 470–522) containing:
  - `actions` (wrapped with safeguards for certain APIs)
  - Node accessors: `getNode`, `getNodes`, `getNodesByType`, `loadNodeContent`
  - Helpers: `createNodeId`, `createContentDigest`
  - GraphQL schema builders
- Captures errors with friendly code frames (lines 560–607) if plugins throw exceptions
- Aggregates results into an array resolved at lines 250–260

This architecture guarantees that `sourceNodes` completes before `onCreateNode` fires, and that `createPages` runs only after the GraphQL schema is stable.

## Complete Implementation Example

Here is a comprehensive [`gatsby-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/gatsby-node.js) demonstrating all three APIs working together:

```javascript
// gatsby-node.js
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
  const posts = await fetchPostsFromCMS();
  
  posts.forEach(post => {
    actions.createNode({
      id: createNodeId(`post-${post.id}`),
      parent: null,
      children: [],
      internal: {
        type: `CMSPost`,
        contentDigest: createContentDigest(post),
      },
      title: post.title,
      rawContent: post.body,
    });
  });
};

exports.onCreateNode = ({ node, actions, getNode }) => {
  if (node.internal.type === `CMSPost`) {
    // Create child Markdown node for remark processing
    const markdownNode = {
      id: `${node.id} >>> MarkdownRemark`,
      parent: node.id,
      children: [],
      internal: {
        type: `MarkdownRemark`,
        mediaType: `text/markdown`,
        contentDigest: node.internal.contentDigest,
      },
      rawMarkdownBody: node.rawContent,
    };
    
    actions.createNode(markdownNode);
    actions.createParentChildLink({ 
      parent: node.id, 
      child: markdownNode.id 
    });
  }
};

exports.createPages = async ({ graphql, actions, reporter }) => {
  const result = await graphql(`
    {
      allMarkdownRemark {
        nodes {
          id
          parent {
            ... on CMSPost {
              title
            }
          }
        }
      }
    }
  `);
  
  if (result.errors) {
    reporter.panicOnBuild(`GraphQL error`, result.errors);
    return;
  }
  
  result.data.allMarkdownRemark.nodes.forEach(node => {
    actions.createPage({
      path: `/posts/${node.id}/`,
      component: require.resolve(`./src/templates/post.js`),
      context: { 
        id: node.id, 
        title: node.parent.title 
      },
    });
  });
};

```

## Summary

- **Bootstrap and Source**: Gatsby loads configurations and executes `sourceNodes` to populate the data layer with raw nodes via `actions.createNode`.
- **Node Enrichment**: The `onCreateNode` API runs synchronously for every node, allowing plugins to add fields or create child nodes before schema generation.
- **Page Generation**: `createPages` queries the finalized GraphQL schema to generate routes, while `createPagesStatefully` handles post-creation logic like 404 pages.
- **Orchestration**: The [`api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/api-runner-node.js) file coordinates execution order, binds actions, and provides error handling across all lifecycle phases.
- **Output**: Webpack configurations in [`packages/gatsby/src/utils/webpack.config.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/webpack.config.js) handle the final `build-html` and `build-javascript` stages to write the `public/` directory.

## Frequently Asked Questions

### What is the difference between createPages and createPagesStatefully in Gatsby?

**`createPages`** is the standard API for generating pages during the initial build phase, receiving `graphql` and `actions` to query data and create routes. **`createPagesStatefully`** runs after the initial page creation is complete and receives the Redux `store`, allowing plugins to inspect the full page list before adding supplemental pages like 404 catch-alls.

### When should I use onCreateNode versus sourceNodes in a Gatsby plugin?

Use **`sourceNodes`** for asynchronous data fetching operations that create initial nodes from external sources like APIs or filesystems. Use **`onCreateNode`** for synchronous transformations of existing nodes, such as adding derived fields, creating child nodes from parent content, or modifying node metadata before the GraphQL schema is compiled.

### How does Gatsby prevent plugins from calling createPage after the createPages API finishes?

In [`packages/gatsby/src/utils/api-runner-node.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/api-runner-node.js) (lines 81–94), Gatsby wraps the `createPage` action with a checker that tracks when the `createPages` promise resolves. If a plugin attempts to call `createPage` after `apiFinished` becomes true (line 100), the runner emits a warning to prevent race conditions and ensure build determinism.

### Where does Gatsby build the final HTML and JavaScript bundles?

The build stages are implemented in [`packages/gatsby/src/utils/webpack.config.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/webpack.config.js), which defines separate webpack configurations for **`build-html`** (line 151) and **`build-javascript`** (line 164). These stages render React components to static HTML and bundle client-side JavaScript, respectively, before writing all assets to the `public/` directory.