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

# Port SQL queries into an Omni workbook via API

> Use a bash script to build Omni tabs and tiles via the Omni 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="February 2026" />

<GuideTitle title="Port SQL queries into an Omni workbook via API" />

This guide shows you how to programmatically create an Omni workbook with multiple SQL-based tabs using a bash script. Automating workbook creation is helpful for generating standardized analysis templates or migrating queries from other systems into Omni.

## Requirements

To follow along with this guide, you'll need:

* [**An Omni API token**](/api/authentication)
* **To install `jq`**, which is a lightweight JSON processor. The script in this guide uses it to build and parse JSON payloads. Refer to the [jq documentation](https://jqlang.org/) for installation instructions.
* **The following information from your Omni instance**:
  * The subdomain of your Omni URL, for example `blobsrus`
  * The ID of the model you want to query

## Implementation

The following script uses `curl` to send a `POST` request to the [`/api/v1/documents` endpoint](/api/documents/create-document). It defines four distinct SQL queries and organizes them into a single workbook with specific visualization types: line charts, bar charts, and tables.

This part of the guide walks through different parts of the script and how they work. Refer to the [Run the script section](#run-the-script) for a full copy-pasteable version of the script.

<Steps>
  <Step title="Script configuration">
    The first part of the script contains your Omni information, which you'll need to define before running the script:

    * `OMNI_INSTANCE` - The subdomain of your Omni instance
    * `OMNI_API_TOKEN` - Your Omni API key
    * `MODEL_ID` - The ID of the model you want to query
    * `DOCUMENT_NAME` - The name of the document you'll create
    * `BASE_TABLE` - The name of an existing view in your Omni model. **This cannot be an arbitrary string**.

          <Note>
            Each query can point to a different base table. The examples in this guide all reference the same `order_items` table for simplicity, but one query could point to `orders` while another points to `products`.

            Omni recommends pointing to a view that is contextually related to the query being defined. While the field primarily serves as a reference point, using an unrelated view may produce unexpected behavior.
          </Note>

    ```bash title="Script configuration" lines theme={null}
    #!/bin/bash

    # =============================================================================
    # CONFIGURATION
    # =============================================================================

    OMNI_INSTANCE="your-instance"
    OMNI_API_TOKEN="XXXXX"
    MODEL_ID="your-model-uuid"
    DOCUMENT_NAME="Multi-Tab SQL Analysis"

    # Base table for connection context
    BASE_TABLE="order_items"
    ```
  </Step>

  <Step title="SQL queries">
    The **SQL queries** section defines the actual SQL queries that will be ported into the workbook.

    ```bash title="SQL queries" lines theme={null}
    # =============================================================================
    # SQL QUERIES
    # =============================================================================

    SQL_QUERY_1="SELECT DATE(created_at) as order_date, SUM(sale_price) as total_revenue FROM order_items GROUP BY 1"
    SQL_QUERY_2="SELECT product_category, SUM(sale_price) as total_sales FROM order_items GROUP BY 1 LIMIT 25"
    SQL_QUERY_3="SELECT status, COUNT(*) as order_count FROM order_items GROUP BY 1"
    SQL_QUERY_4="SELECT DATE_TRUNC('month', created_at) as month, SUM(sale_price) as revenue FROM order_items GROUP BY 1"
    ```
  </Step>

  <Step title="API request">
    <Tip>
      Use the **Model ID** found in the URL of your model IDE to ensure the workbook targets the correct data schema.
    </Tip>

    The **API request** portion of the script uses the information in the **Configuration** and **SQL queries** sections to build the curl request.

    The `queryPresentations` array defines the individual query tabs (tiles) within the workbook. Each query tab object contains a few parameters that are worth highlighting:

    * `isSql: true` - This parameter tells Omni to treat the `userEditedSQL` string as the primary source of truth for the query
    * `visConfig` - This parameter sets the default visualization style for the query tab, such as `line`, `bar`, or `table`

    The following snippet highlights these parameters in the script:

    ```bash title="API request" lines highlight={19,22,24} theme={null}
    # =============================================================================
    # API REQUEST
    # =============================================================================

    API_URL="https://${OMNI_INSTANCE}.omniapp.co/api/v1/documents"

    # Build JSON payload using jq
    JSON_PAYLOAD=$(jq -n \
      --arg modelId "$MODEL_ID" \
      --arg name "$DOCUMENT_NAME" \
      --arg table "$BASE_TABLE" \
      --arg sql1 "$SQL_QUERY_1" \
      --arg sql2 "$SQL_QUERY_2" \
      --arg sql3 "$SQL_QUERY_3" \
      --arg sql4 "$SQL_QUERY_4" \
      '{
        modelId: $modelId,
        name: $name,
        queryPresentations: [
          {
            name: "Daily Revenue",
            isSql: true,
            query: { table: $table, fields: [], userEditedSQL: $sql1 },
            visConfig: { chartType: "line" }
          },
          {
            name: "Top Products",
            isSql: true,
            query: { table: $table, fields: [], userEditedSQL: $sql2 },
            visConfig: { chartType: "bar" }
          },
          {
            name: "Order Status",
            isSql: true,
            query: { table: $table, fields: [], userEditedSQL: $sql3 },
            visConfig: { chartType: "table" }
          },
          {
            name: "Monthly Trends",
            isSql: true,
            query: { table: $table, fields: [], userEditedSQL: $sql4 },
            visConfig: { chartType: "area" }
          }
        ]
      }')

    # Send the request
    curl -X POST "$API_URL" \
      -H "Authorization: Bearer ${OMNI_API_TOKEN}" \
      -H "Content-Type: application/json" \
      -d "$JSON_PAYLOAD"
    ```
  </Step>
</Steps>

## Run the script

The following code example is a full copy-pasteable version of the script. To use it:

1. Save this version to a convenient location.
2. Replace the configuration variables with your Omni information.
3. Use the command line to run the script!

```bash title="Full copy-pasteable script version" expandable theme={null}
#!/bin/bash

# =============================================================================
# CONFIGURATION
# =============================================================================

OMNI_INSTANCE="your-instance"
OMNI_API_TOKEN="XXXXX"
MODEL_ID="your-model-uuid"
DOCUMENT_NAME="Multi-Tab SQL Analysis"

# Base table for connection context
BASE_TABLE="order_items"

# =============================================================================
# SQL QUERIES
# =============================================================================

SQL_QUERY_1="SELECT DATE(created_at) as order_date, SUM(sale_price) as total_revenue FROM order_items GROUP BY 1"
SQL_QUERY_2="SELECT product_category, SUM(sale_price) as total_sales FROM order_items GROUP BY 1 LIMIT 25"
SQL_QUERY_3="SELECT status, COUNT(*) as order_count FROM order_items GROUP BY 1"
SQL_QUERY_4="SELECT DATE_TRUNC('month', created_at) as month, SUM(sale_price) as revenue FROM order_items GROUP BY 1"

# =============================================================================
# API REQUEST
# =============================================================================

API_URL="https://${OMNI_INSTANCE}.omniapp.co/api/v1/documents"

# Build JSON payload using jq
JSON_PAYLOAD=$(jq -n \
  --arg modelId "$MODEL_ID" \
  --arg name "$DOCUMENT_NAME" \
  --arg table "$BASE_TABLE" \
  --arg sql1 "$SQL_QUERY_1" \
  --arg sql2 "$SQL_QUERY_2" \
  --arg sql3 "$SQL_QUERY_3" \
  --arg sql4 "$SQL_QUERY_4" \
  '{
    modelId: $modelId,
    name: $name,
    queryPresentations: [
      {
        name: "Daily Revenue",
        isSql: true,
        query: { table: $table, fields: [], userEditedSQL: $sql1 },
        visConfig: { chartType: "line" }
      },
      {
        name: "Top Products",
        isSql: true,
        query: { table: $table, fields: [], userEditedSQL: $sql2 },
        visConfig: { chartType: "bar" }
      },
      {
        name: "Order Status",
        isSql: true,
        query: { table: $table, fields: [], userEditedSQL: $sql3 },
        visConfig: { chartType: "table" }
      },
      {
        name: "Monthly Trends",
        isSql: true,
        query: { table: $table, fields: [], userEditedSQL: $sql4 },
        visConfig: { chartType: "area" }
      }
    ]
  }')

# Send the request
curl -X POST "$API_URL" \
  -H "Authorization: Bearer ${OMNI_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "$JSON_PAYLOAD"
```

## Next steps

Once you run the script, the API will return a JSON object containing a `workbook` identifier. You can navigate to `https://[instance].omniapp.co/dashboards/[identifier]` to view your new multi-tab analysis.
