> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omni.co/llms.txt
> Use this file to discover all available pages before exploring further.

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.omni.co/feedback

```json
{
  "path": "/guides/api/data-lineage-integration",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# Build a data lineage integration with the Omni API

> Build OpenMetadata / DataHub integrations using the Omni REST API

export const categoryIcons = {
  'administration': 'lock',
  'api': 'terminal',
  'connections': 'database',
  'dashboards': 'table-columns',
  'embed': 'code',
  'errors': 'exclamation',
  'modeling': 'wrench',
  'patterns': 'plus',
  'schedules & alerts': 'envelope',
  'visualizations': 'chart-column',
  'workbooks': 'book'
};

export const GuideSidebar = ({category, relatedLinks, updatedDate}) => {
  const [progress, setProgress] = React.useState(0);
  React.useEffect(() => {
    const sidebar = document.querySelector('.guide-sidebar');
    if (!sidebar) return;
    let container = sidebar.parentElement;
    while (container && !container.querySelector('.guide-header')) {
      container = container.parentElement;
    }
    if (container && !container.classList.contains('guide-page-layout')) {
      container.classList.add('guide-page-layout');
    }
  }, []);
  React.useEffect(() => {
    const handleScroll = () => {
      const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const scrollPercent = docHeight > 0 ? scrollTop / docHeight * 100 : 0;
      setProgress(Math.min(100, Math.max(0, scrollPercent)));
    };
    window.addEventListener('scroll', handleScroll, {
      passive: true
    });
    handleScroll();
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  const icon = category ? categoryIcons[category.toLowerCase()] || 'book' : 'book';
  return <aside className="guide-sidebar">
      <div className="guide-sidebar-content">
        <a href="/guides" className="guide-sidebar-back">
          <Icon icon="arrow-left" iconType="solid" size={14} />
          <span>All guides</span>
        </a>

        <div className="guide-sidebar-section">
          <div className="guide-sidebar-label">Progress</div>
          <div className="guide-sidebar-progress">
            <div className="guide-mascot">
              <svg viewBox="0 0 450 450" width="48" height="48">
                <defs>
                  <clipPath id="progressClip">
                    <rect x="0" y={450 - progress * 4.5} width="450" height={progress * 4.5} />
                  </clipPath>
                  <linearGradient id="blobbyGradient" x1="55.9753" y1="0" x2="492.197" y2="169.724" gradientUnits="userSpaceOnUse">
                    <stop stopColor="#BCA2F3" />
                    <stop offset="0.572917" stopColor="#FF7AA2" />
                    <stop offset="1" stopColor="#F3D4A2" />
                  </linearGradient>
                </defs>

                {}
                <circle cx="223.901" cy="223.901" r="213.901" transform="matrix(-0.999988 -0.0049013 0.00491945 -0.999988 447.797 449.992)" fill="#FAFAFA" stroke="#480B38" strokeWidth="20" />

                {}
                <circle cx="223.901" cy="223.901" r="213.901" transform="matrix(-0.999988 -0.0049013 0.00491945 -0.999988 447.797 449.992)" fill="url(#blobbyGradient)" stroke="#480B38" strokeWidth="20" clipPath="url(#progressClip)" />

                {}
                <path d="M310.41 195.084C310.41 200.052 301.362 212.472 284.328 212.472C266.585 212.472 258.246 201.294 258.246 195.912" stroke="#480B38" strokeWidth="17.3883" strokeMiterlimit="1.33344" strokeLinecap="round" />
                <circle cx="21.168" cy="21.168" r="21.168" transform="matrix(-1 0 0 1 388.658 169.001)" fill="#480B38" />
                <circle cx="21.168" cy="21.168" r="21.168" transform="matrix(-1 0 0 1 223.467 169.001)" fill="#480B38" />
              </svg>
            </div>
            <span className="guide-sidebar-progress-text">{Math.round(progress)}%</span>
          </div>
        </div>

        {category && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Category</div>
            <div className="guide-sidebar-category">
              <Icon icon={icon} iconType="solid" size={14} />
              <span>{category}</span>
            </div>
          </div>}

        {updatedDate && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Last updated</div>
            <div className="guide-sidebar-date">{updatedDate}</div>
          </div>}

        {relatedLinks && relatedLinks.length > 0 && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Related</div>
            <ul className="guide-sidebar-links">
              {relatedLinks.map((link, index) => <li key={index}>
                  <a href={link.href}>{link.title}</a>
                </li>)}
            </ul>
          </div>}
      </div>
    </aside>;
};

export const GuideTitle = ({title}) => {
  return <div className="guide-header">
      <h1 className="guide-title">{title}</h1>
    </div>;
};

<GuideSidebar
  categoryIcons={categoryIcons}
  category="api"
  updatedDate="March 2026"
  relatedLinks={[
{ title: "Run document queries with the Query API", href: "/guides/api/run-document-queries" },
{ title: "API authentication", href: "/api/authentication" },
{ title: "API rate limits", href: "/api/rate-limits" },
{ title: "API Explorer", href: "/api/api-explorer" }
]}
/>

<GuideTitle title="Build a data lineage integration with the Omni API" />

Integrating Omni with metadata catalogs like OpenMetadata or DataHub allows you to visualize how data flows from raw database tables into Omni dashboards. By using the Omni REST API, you can construct a complete, queryable lineage graph that gives you a picture of how data is consumed across your organization.

The lineage will follow this structure:

```mermaid actions={false} theme={null}
flowchart LR
  A["Folder"] --> B["Dashboard"] --> C["Tile (Query)"] --> D["Topic"] --> E["View"] --> F["Database Table/Column"]
```

The approach in this guide is read-only and relies on endpoints that retrieve information from your Omni instance:

| Entity         | Endpoint                        | Purpose                                                  |
| :------------- | :------------------------------ | :------------------------------------------------------- |
| **Folders**    | `GET /v1/folders`               | Lists all top-level and nested folders.                  |
| **Documents**  | `GET /v1/documents`             | Lists dashboards and workbooks within a folder.          |
| **Queries**    | `GET /v1/documents/:id/queries` | Retrieves the underlying queries (tiles) for a document. |
| **Model YAML** | `GET /v1/models/:id/yaml`       | Retrieves the view definitions and SQL mappings.         |
| **Topics**     | `GET /v1/models/:id/topics`     | Lists the available entry points for exploration.        |

## Requirements

To follow the steps in this guide, you'll need:

* **Modeler** or **Connection Admin** permissions on the Omni model you want to work with
* [**A Personal Access Token**](/api/authentication#personal-access-tokens-pat) for the Omni API

## Steps

<Tip>
  **Remember to update example placeholders!** Throughout this guide, you'll see variables like `{your-omni-instance}` in code examples. These are placeholders that you'll need to replace with real values when you run the code yourself.
</Tip>

<Steps>
  <Step title="Retrieve a list of folders" titleSize="h3">
    Folders serve as the top-level nodes of your lineage graph. Use the [List folders](/api/folders/list-folders) endpoint to retrieve all folders in your organization.

    ```bash title="GET /api/v1/folders" theme={null}
    curl --request GET \
    --url 'https://{your-omni-instance}.omniapp.co/api/v1/folders' \
    --header 'Authorization: Bearer {your-omni-token}'
    ```

    The response from this endpoint uses cursor-based pagination. Loop until `pageInfo.hasNextPage` is `false`.

    Then, store the following fields:

    * `id` - Used to filter documents in the next step
    * `name` - The human-readable folder name
    * `url` - A direct link to the folder in Omni
  </Step>

  <Step title="List documents per folder" titleSize="h3">
    Next, you'll retrieve the documents in each folder.

    For every folder `id` you retrieved in the previous step, call the [List documents](/api/documents/list-documents) endpoint. Replace `{folder_id}` with the `id` of a folder:

    ```bash title=" GET /api/v1/documents?folderId={folder_id}" theme={null}
    curl --request GET \
    --url 'https://{your-omni-instance}.omniapp.co/api/v1/documents?folderId={folder_id}' \
    --header 'Authorization: Bearer {your-omni-token}'
    ```

    Then, store the following fields from the response - you'll use them to identify content items in a later step:

    * `identifier` - The document ID used in subsequent calls
    * `name` - The human-readable document name
    * `url` - A direct deep-link to the dashboard or workbook
    * `hasDashboard` - Indicates if the document contains tiles
    * `type` - Usually `document` for workbooks and dashboards
  </Step>

  <Step title="List queries per document" titleSize="h3">
    Next, you'll retrieve the queries in each document. Each tile on a dashboard corresponds to a workbook query.

    For every document `identifier` you retrieved in the previous step, call the [Get document queries](/api/documents/get-document-queries) endpoint. Replace `{documentId}` with the `identifier` of a document:

    ```bash title="GET /api/v1/documents/{documentId}/queries" theme={null}
    curl --request GET \
    --url 'https://{your-omni-instance}.omniapp.co/api/v1/documents/{documentId}/queries' \
    --header 'Authorization: Bearer {your-omni-token}'
    ```

    The response will include a top-level `queries` object, which will contain a list of queries. For example:

    ```json title="Example response from the List document queries endpoint" theme={null}
    {
      "queries": [
        {
          "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
          "name": "Blobtastic Dashboard",
          "url": "https://{your-omni-org}.omni.co/w/abc123?key=1",
          "query": {...}
        }
      ]
    }
    ```

    The full `query` object isn't included here due to length, but you'll need to store the following fields from the response:

    * `url` - A direct deep-link to the specific tile/query
    * `query.table` - The Omni view reference in `schema__viewname` format (e.g., `ecomm__order_items`)
    * `query.fields` - Array of field references (e.g., `schema__viewname.field_name`)
    * `query.join_paths_from_topic_name` - The topic label (e.g., "Orders"). Identifies which topic the tile belongs to.
    * `query.modelId` - Use this ID when calling the Get model YAML endpoint in the next step
  </Step>

  <Step title="Resolve topics and views from model YAML" titleSize="h3">
    In this step, you'll map Omni objects to raw database tables using the model's underlying configuration.

    * **Topics** define the join graph and are the entry point for exploration
    * **Views** map to a database table or query.

    The `join_paths_from_topic_name` field identifies the topic, which usually shares the name of the base view.

    Because model logic is often split across multiple files, you'll start by finding the correct file path for a specific view.

    1. **Retrieve the view map**. Use the `modelId` from the previous step to call the [Get model YAML](/api/models/get-model-yaml) endpoint as follows:

       ```bash title="GET /api/v1/models/{modelId}/yaml" theme={null}
       curl --request GET \
       --url 'https://{your-omni-instance}.omniapp.co/api/v1/models/{modelId}/yaml' \
       --header 'Authorization: Bearer {your-omni-token}'
       ```

       The response will contain a `viewNames` map that indexes every view file path by its internal `schema__viewname` reference.
    2. **Locate the file in the map**. Search the map for the view name provided in the query (e.g., `ecomm__order_items`).

           <Note>
             **Omni supports two types of views.** Standard views map to physical tables, while [query views](/modeling/query-views) are promoted from saved queries. Check the `viewNames` map to determine if you need to request a `.view` or `.query.view` file.
           </Note>
    3. **Fetch the YAML for the file**. After you find the file name, use it in the `fileName` parameter to retrieve the file's YAML:

       ```bash title="GET /api/v1/models/{modelId}/yaml?fileName={schema_name}/{view_name}.view" theme={null}
       curl --request GET \
       --url 'https://{your-omni-instance}.omniapp.co/api/v1/models/{modelId}/yaml?fileName={SCHEMA_NAME}/{view_name}.view' \
       --header 'Authorization: Bearer {your-omni-token}'
       ```

           <Note>
             The `fileName` parameter is case-sensitive and requires the schema portion to be **uppercase** to resolve correctly (e.g., `ECOMM/order_items.view`).
           </Note>

    Once you have the specific YAML content, extract these fields from the response to complete the mapping from Omni to your database:

    * `table_name` - The physical table name in your database
    * `schema` - The database schema where the table resides
    * `dimensions` and `measures` - These blocks contain a `sql` property that defines which database columns are used for that field
  </Step>

  <Step title="Assemble the lineage graph" titleSize="h3">
    Once all data is collected, you can assemble the lineage graph:

    1. **Add deep links to the catalog**. Use the `url` fields returned directly in the response payloads to provide deep-links in your catalog:

       * **Folders** - `https://{your-omni-instance}.omniapp.co/f/{folder_id}`
       * **Documents** - `https://{your-omni-instance}.omniapp.co/dashboards/{document_id}`
       * **Tiles/Queries** - `https://{your-omni-instance}.omniapp.co/w/{document_id}?key={tile_key}`

           <Note>
             While URLs can be constructed manually, it's best practice to use the `url` property returned by the API as it handles the correct routing for different document types.
           </Note>

    2. **Create the graph**. Use the following diagram to assemble your graph. Each arrow represents a foreign-key relationship established through the API traversal you completed in previous steps.

       ```mermaid actions={false} theme={null}
       flowchart TD
         A["Folder (id, name)"] -->|"List documents (folderId filter)"| B["Dashboard / Document (identifier, name)"]
         B -->|"Get document queries"| C["Tile / Query (query id, table, fields)"]
         C -->|"join_paths_from_topic_name"| D["Topic (topic name = base view name)"]
         D -->|"Model YAML (view files)"| E["View (.view or .query.view YAML)"]
         E -->|"sql_table_name in view YAML"| F["Database table / column"]
       ```

       By parsing the `query.fields` array and comparing it to the `dimensions` and `measures` in your YAML, you can identify exactly which database columns drive each dashboard tile.

           <Tip>
             When building your graph, use the `identifier` or `id` fields as unique keys for each node to ensure updates or deletions in Omni are correctly reflected.
           </Tip>
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Can't access model / missing folders in API response">
    Personal Access Tokens have the same permissions as the user that created the token.

    If you can't access a model or you see folders missing in API responses, the user making the requests may not have permissions to those objects in Omni. Verify that the owner of the token has the correct permissions.
  </Accordion>

  <Accordion title="'Too many requests' error from API">
    If you receive a `429 too many requests error` from the API, you'll need to implement exponential backoff to avoid hitting the [rate limit](/api/rate-limits).
  </Accordion>

  <Accordion title="Folder or document responses are incomplete">
    The List folders and List documents endpoints use pagination in their responses, which means only a specific number of folders or documents are included per 'page'.

    If it seems like the response is incomplete, check the value of `pageInfo.hasNextPage` in the response. If `true`, you'll need to make subsequent requests and pass the `nextCursor` value as the `cursor` parameter to retrieve the next page of results.
  </Accordion>
</AccordionGroup>

## Next steps

* **Explore the API reference** - Review the full [API reference](/api) for endpoints used in this guide
* **Drafts and publishing** - Learn how to use [drafts](/content/develop/drafts) to stage model changes before deploying
