---
title: "Data sources for lookup"
description: "Create reusable lookup tables and reference them in expressions with the lookup() function."
url: https://docs.woolytech.com/en/articles/245-data-sources-for-lookup
collection: "Add-ons"
updated: 2026-11-03
language: en
---

# Data sources for lookup

Create reusable lookup tables and reference them in expressions with the lookup() function.

**Data sources for lookup** lets you create reusable lookup tables and reference them in your feed expressions with the `lookup()` function. This lets you inject external data (price sheets, category codes, translated labels, sales figures, and more) into each product when the feed is built, without entering values by hand.

Open the feature at **Settings → Data sources** (`/settings/datasources`).

* * *

## On this page

-   [When to use it](#when-to-use-it)
    
-   [How it works](#how-it-works)
    
-   [Data source types](#data-source-types)
    
-   [Common options](#common-options)
    
-   [Using it in an expression with lookup()](#using-it-in-an-expression-with-lookup)
    
-   [Testing with the Test button](#testing-with-the-test-button)
    
-   [Cache and performance](#cache-and-performance)
    
-   [Credits and billing](#credits-and-billing)
    
-   [Troubleshooting](#troubleshooting)
    
-   [FAQ](#faq)
    

* * *

## When to use it

Use a data source when you have a **key → value** table and need to look up that value for each product/variant while the feed is built. For example:

-   A dedicated price list per SKU or per product group.
    
-   A mapping from Shopify product codes to internal supplier IDs / GTINs.
    
-   A table for translating category names or custom labels.
    
-   Sales figures pulled from Shopify (ShopifyQL) to flag "best sellers".
    

In short: if you need to "look up a value by key" for every product, use a data source instead of hard-coding it into each rule.

* * *

## How it works

1.  You declare a data source with a **Name**, **Type**, **Key column**, **Value column**, and a data origin (a CSV URL, a Google Sheet URL, or a Shopify query).
    
2.  When the feed is built, the app reads the origin and builds a lookup table named `t#<Name>` — for example, a data source named `Pricing sheet` produces the token `t#Pricing sheet`.
    
3.  In a feed expression you call `lookup("{token}", 't#<Name>')` to fetch the matching value.
    

**Key points:**

-   In the expression editor (RuleEditor), these tables appear under the **Lookup tables** group, shown as `Table "<Name>"`.
    
-   Only data sources that are **actually referenced** in a feed's expressions (via the `t#<Name>` string) are loaded and built. Unused data sources cost no build resources.
    
-   Data sources are **not built for feeds with Type = API** (only file-based feeds apply: CSV, TSV, XML, JSON, etc.).
    
-   Each data source must have a **unique Name** (case-insensitive).
    

* * *

## Data source types

### 1\. CSV file

Use a CSV/TSV file hosted at a public URL.

<table data-id="73a750a7-acba-4f47-a126-9a64e9b9c9ca" style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p data-id="5396ad0a-cb01-4962-a182-129c0e4386aa">Field</p></th><th colspan="1" rowspan="1"><p data-id="a57b378d-cec3-4287-927e-b6ffe606f7c2">Required</p></th><th colspan="1" rowspan="1"><p data-id="6f8f15b6-8dc9-44f4-ba22-6a3e91e2f217">Notes</p></th></tr><tr><td colspan="1" rowspan="1"><p data-id="9cba39bb-0315-4da7-a60d-ded4610b535d">CSV file URL</p></td><td colspan="1" rowspan="1"><p data-id="77d9e1bd-1ddc-4e0a-8d67-8f7e61034fa9">Yes</p></td><td colspan="1" rowspan="1"><p data-id="560ac712-ca02-49b9-8ac1-93d0e7317fc1">Must be an <code>http://</code> or <code>https://</code> URL</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="24bd44a1-3c75-493f-a1b0-93546db2a349">Key column</p></td><td colspan="1" rowspan="1"><p data-id="678f1f6f-df3c-4f9e-a547-6a662a0e8883">No</p></td><td colspan="1" rowspan="1"><p data-id="c25444a1-5f72-4325-8fec-744e6b8cfa51">Defaults to <code>1</code></p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="17a4c8ed-6505-4185-9a13-a42f548d11aa">Value column</p></td><td colspan="1" rowspan="1"><p data-id="6b8860c7-fdf8-417f-be44-eaefe0519e8f">No</p></td><td colspan="1" rowspan="1"><p data-id="db2f602a-3ba8-48ed-b034-730e25a54271">Defaults to <code>2</code></p></td></tr></tbody></table>

-   The delimiter is auto-detected from the URL: if the URL contains `tqx=out:tsv`, ends with `.tsv`, or includes `format=tsv`, a **tab** is used; otherwise a **comma**.
    

### 2\. Google sheet

Use a publicly shared Google Sheet.

<table data-id="5ca5d897-682c-428f-b328-86def310d1b4" style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p data-id="4190c206-0df1-4140-8867-e426f529d3a2">Field</p></th><th colspan="1" rowspan="1"><p data-id="898088fa-1779-49c7-ae1d-6a30b4da8d47">Required</p></th><th colspan="1" rowspan="1"><p data-id="00890e5d-f319-4122-a9cd-ed01c93cc390">Notes</p></th></tr><tr><td colspan="1" rowspan="1"><p data-id="11a505e1-751a-4514-a23c-1c8152a6705c">Google sheet URL</p></td><td colspan="1" rowspan="1"><p data-id="1d1396a8-680b-4689-b852-b207035c383c">Yes</p></td><td colspan="1" rowspan="1"><p data-id="fa1e5c40-5a0c-4cdf-a180-1f3fd43aabd6">Format: <code>https://docs.google.com/spreadsheets/d/{sheetId}/edit</code></p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="0b4abb27-b213-4023-acff-bc6f2f4b7058">Sheet title</p></td><td colspan="1" rowspan="1"><p data-id="bee1fd89-a6e9-4d4f-8b46-1833f5b32e35">No</p></td><td colspan="1" rowspan="1"><p data-id="8afae593-86a3-4824-855b-6012b0899b99">Leave blank to use the first sheet</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="3465cbd0-77d7-46bc-b100-308fad2a7444">Key column</p></td><td colspan="1" rowspan="1"><p data-id="e4b8d270-08df-46a5-abd3-d8f925391eeb">No</p></td><td colspan="1" rowspan="1"><p data-id="2381c505-90d4-448c-82d5-9adcc8ce86c8">Defaults to <code>1</code></p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="b83fb253-1a8b-47d5-9f60-89ee57db3bbf">Value column</p></td><td colspan="1" rowspan="1"><p data-id="8d071c0f-d6e8-42e7-b68f-e8cf780eb25b">No</p></td><td colspan="1" rowspan="1"><p data-id="e787c5ff-df92-4de2-8078-754816f8d450">Defaults to <code>2</code></p></td></tr></tbody></table>

-   The sheet must be shared as **"Anyone with the link"**.
    
-   Data is read through Google's CSV export endpoint (`gviz/tq?tqx=out:csv`), so the sheet should be a table with a clear header row.
    

### 3\. Shopify query

Use a **ShopifyQL** query to pull figures from Shopify.

<table data-id="df4edb52-9777-4296-87df-3a12b32b2a9d" style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p data-id="5b8fe835-e1ad-4df7-8a1f-e33d6aeed641">Field</p></th><th colspan="1" rowspan="1"><p data-id="6854e8dc-9c26-4c07-9d63-aee935f1b8c3">Required</p></th><th colspan="1" rowspan="1"><p data-id="f1f4aa13-4f1b-4f28-944e-482e2358363b">Notes</p></th></tr><tr><td colspan="1" rowspan="1"><p data-id="4d38a9b1-aa51-467b-a5c5-6f3c8ef355ec">Query string</p></td><td colspan="1" rowspan="1"><p data-id="f17e8d54-8218-405a-b197-f186b29e97bd">Yes</p></td><td colspan="1" rowspan="1"><p data-id="bd46936b-066e-46b6-8c35-4c8f84c22677">Example: <code>FROM sales SHOW product_id, total_sales GROUP BY product_id SINCE -90d LIMIT 1000</code></p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="6124a0e9-1572-4f8e-bae2-34682d36c574">Key column</p></td><td colspan="1" rowspan="1"><p data-id="99dd9278-c21e-49cf-91ee-000a8c95a74e">No</p></td><td colspan="1" rowspan="1"><p data-id="080e78ef-c5aa-45a0-b336-a2219824436f">Defaults to <code>1</code></p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="9f6bb7fa-d4e9-4b78-89b2-bb8b9e525deb">Value column</p></td><td colspan="1" rowspan="1"><p data-id="5e50016e-3270-4bd4-80f1-c9d1dc28d71f">No</p></td><td colspan="1" rowspan="1"><p data-id="cc7186bd-84ce-4c73-8b66-668d081712dc">Defaults to <code>2</code></p></td></tr></tbody></table>

-   This type requires the `read_reports` scope. If it is missing, the UI shows an **Additional permission required** banner with a **Grant access** button.
    
-   The result's header row is used as column names; **Key column** and **Value column** are 1-based positions in the returned result.
    

* * *

## Common options

Every data source has the following fields (in the top-right of each block):

-   **Name** — identifier, **required** and **must be unique**. This name is the part after `t#` in the reference token.
    
-   **Type** — `CSV file`, `Google sheet`, or `Shopify query`.
    
-   **Key column** — 1-based column used as the key.
    
-   **Value column** — 1-based column holding the returned value.
    

Status badges for each block:

<table data-id="2f44cdac-b821-441f-8f1c-501018217fbf" style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p data-id="1cea293e-62cc-4bee-809e-3c1b90bdba6f">Badge</p></th><th colspan="1" rowspan="1"><p data-id="90fa3a98-21a0-484b-a371-2edc94213e99">Meaning</p></th></tr><tr><td colspan="1" rowspan="1"><p data-id="a2feffc6-18bf-4407-ae38-a17a81cc1dbe"><strong>New</strong></p></td><td colspan="1" rowspan="1"><p data-id="3e4f4cf4-7ac0-44db-9991-aed894ea1cc2">Data source just added, not saved yet</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="f64a4fd9-223d-4043-9042-65c2ad9effe0"><strong>Modified</strong></p></td><td colspan="1" rowspan="1"><p data-id="8d1f8dd5-50d0-4274-a1cf-bc4a6937f01c">Has unsaved changes</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="b1a9e2ff-978e-477c-bb15-4df1e63b4a9c"><strong>Free</strong></p></td><td colspan="1" rowspan="1"><p data-id="be923a6c-c15a-4883-b3a0-e8f12b5a140c">Within the shop's free data source allowance</p></td></tr></tbody></table>

Click **Save** on the save bar (ContextualSaveBar) to save; **Discard** to revert changes.

* * *

## Using it in an expression with lookup()

Syntax:

```text
lookup(key, map, arrayOption?)

```

<table data-id="1aa8b6ea-f539-4d29-8064-ad9c1eb2d7f0" style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p data-id="01c7df55-1085-4a49-8a03-29933c1e4c61">Parameter</p></th><th colspan="1" rowspan="1"><p data-id="c8befb2b-66b6-4a45-b607-01596f3e246c">Description</p></th></tr><tr><td colspan="1" rowspan="1"><p data-id="3952ed41-9ea3-4284-bf1c-400386fdd62c"><code>key</code></p></td><td colspan="1" rowspan="1"><p data-id="f82b84ed-ad39-42d0-992f-5c8aa1188c8d">The value to look up. Usually a product token, e.g. <code>'{sku}'</code>, <code>'{product_id}'</code>, <code>'{g:id}'</code>.</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="80a483d3-1423-409b-a4b0-8cc108708b50"><code>map</code></p></td><td colspan="1" rowspan="1"><p data-id="2aed7199-08b0-408d-beeb-e5c8ed80b2ae">The table reference as a single-quoted string: <code>'t#&lt;Name&gt;'</code>, e.g. <code>'t#Pricing sheet'</code>.</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="bc00d547-3983-4df4-986a-d0d59bd7ab41"><code>arrayOption</code></p></td><td colspan="1" rowspan="1"><p data-id="4205a019-f4e3-4e69-859f-05e167d05467">(Optional) How to handle a key that appears multiple times: <code>'array'</code>, <code>'first'</code>, <code>'last'</code>. Omitted returns the raw value.</p></td></tr></tbody></table>

Example:

```text
lookup('{sku}', 't#Pricing sheet')

```

If a key appears on multiple rows, values are grouped; use `arrayOption` to choose:

```text
lookup('{product_id}', 't#Best sellers', 'first')

```

**Inserting the token quickly:** in the RuleEditor, open the token list and pick under **Lookup tables** → `Table "<Name>"`. The `t#<Name>` token is inserted into the expression.

Besides `lookup()`, you can reference a table as a variable using the `t#<Name>` token itself where supported (the table is added to the expression context during a build).

* * *

## Testing with the Test button

Each data source has a **Test** button that opens a dialog:

1.  The app loads up to the **first 1,000 rows** from the origin (`LIMIT_SAMPLE_DATA`).
    
2.  Enter a **Key** to test the lookup.
    
3.  See the result and the matching **expression sample**:
    

```text
lookup('{key}', 't#<Name>') → "value"

```

4.  Below, the **first 100 rows** of the loaded data are shown (`DISPLAY_SAMPLE_DATA`) so you can verify the columns.
    

Result states:

<table data-id="61827d32-e761-4f43-8d0c-7987b85f6141" style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p data-id="e9e63a5a-e2a2-4b61-87e8-0587b8bdc7e1">Display</p></th><th colspan="1" rowspan="1"><p data-id="889fbf8b-f1c6-450d-bd8e-338534dc7d6a">Meaning</p></th></tr><tr><td colspan="1" rowspan="1"><p data-id="0126779e-f079-459b-978c-25aac9097e97"><strong>Not found</strong></p></td><td colspan="1" rowspan="1"><p data-id="f2635e38-97e0-49fb-b457-78366340c493">The key was not found in the data</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="861a5c0a-1f01-4004-a320-0ac1cdc3cd94"><strong>No data</strong></p></td><td colspan="1" rowspan="1"><p data-id="795bee27-49f5-4388-8e98-fed5fdb2361b">The key was found but the <strong>Value column</strong> cell is empty</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="d1ce436f-e26b-4d66-85a9-acfddc494293">A normal value</p></td><td colspan="1" rowspan="1"><p data-id="00e38681-bdaa-4229-bfa7-ff74f8483205">Lookup succeeded</p></td></tr></tbody></table>

Note: the Test dialog only searches the **first 1,000 loaded rows**, not the entire origin.

* * *

## Cache and performance

-   Lookup tables are **cached for 30 minutes** under the key `s#{shopOrigin}:{t#Name}`. Within the cache window, the feed reuses the built table instead of re-fetching the origin.
    
-   To apply fresh data immediately, wait for the cache to expire or re-run the feed after the origin has been updated.
    
-   Only data sources used in a feed are built → use clear names and remove tables you no longer use.
    

* * *

## Credits and billing

-   The feature is a **metered add-on**: **1 credit per data source** in use.
    
-   A shop may be granted a number of **free data sources** (`freeDataSourceUnits`). Data sources within this allowance show the **Free** badge.
    
-   When the number of data sources exceeds the free allowance, clicking **Save** triggers **billing**: the app moves to Shopify's subscription approval flow if the available credits are insufficient. After approval, the change is saved automatically.
    
-   There is **no usage charge** for data sources (credits only).
    

* * *

## Troubleshooting

<table data-id="8bf6e01b-7d0c-4542-9ef7-af2ba6d97ed9" style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p data-id="3d03b43b-2fb1-4ff8-a46d-5e56d99548ca">Symptom</p></th><th colspan="1" rowspan="1"><p data-id="b8920c2a-1103-44e9-ad2b-76811cbb04fe">Likely cause</p></th><th colspan="1" rowspan="1"><p data-id="ac92b7eb-82c8-481a-9bc3-0ac261ee1b46">Fix</p></th></tr><tr><td colspan="1" rowspan="1"><p data-id="60785748-724c-4d70-9d84-0d8921a42e2d"><code>LookupError</code> while building the feed</p></td><td colspan="1" rowspan="1"><p data-id="302686d6-a3ed-4402-b3f5-adf3da0e9743">The origin failed (bad URL, private sheet, invalid query)</p></td><td colspan="1" rowspan="1"><p data-id="9fdb5518-2db0-4523-910c-13e93d1d860d">Open <strong>Test</strong> to see the specific error; check the URL / sharing settings / query syntax</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="63c7d377-50ad-4f28-a1ec-a42185144452">Lookup always returns "Not found"</p></td><td colspan="1" rowspan="1"><p data-id="c85e7e1f-aa2c-4bf1-bac4-a8af18b36bde">Wrong <strong>Key column</strong>, or the key does not match</p></td><td colspan="1" rowspan="1"><p data-id="f2eaccdf-4d8d-4979-9672-b0062732c5f6">Verify the key column in the data; check the value passed as the key</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="d17ed22c-9924-44c1-b09e-41548c2cee6e">Empty result even though the key exists</p></td><td colspan="1" rowspan="1"><p data-id="8a4d0630-a6cd-4fb5-bec3-2d8b48a950bc">Wrong <strong>Value column</strong>, or the value cell is empty</p></td><td colspan="1" rowspan="1"><p data-id="921797ce-8d70-4061-8c73-90eee40819c2">Check <strong>Value column</strong> (1-based); the <strong>No data</strong> badge points to the value column</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="eb650c21-6015-44f7-9ddd-479a0052592f">Google Sheet cannot be read</p></td><td colspan="1" rowspan="1"><p data-id="2fdceec1-3e01-4399-a026-c58d687bb375">The sheet is not shared publicly</p></td><td colspan="1" rowspan="1"><p data-id="77772c91-bef6-4fde-95a6-16832e442e8b">Change sharing to "Anyone with the link"</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="c2ce7a4f-5429-428e-8cc6-61b72f910eed">Shopify query is blocked</p></td><td colspan="1" rowspan="1"><p data-id="e1b2097f-b19d-4748-b14a-51529dcef5ad">Missing the <code>read_reports</code> scope</p></td><td colspan="1" rowspan="1"><p data-id="6d460aeb-1c7f-475d-9470-f0a0baf0a08b">Click <strong>Grant access</strong> on the banner to grant it</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="8c3a4e7d-336a-4ccd-ae8c-81921739a62d">Old data keeps appearing</p></td><td colspan="1" rowspan="1"><p data-id="61d0dc9a-8888-4f72-9fc4-1e4f562098bd">The table is within the 30-minute cache</p></td><td colspan="1" rowspan="1"><p data-id="9d189223-24b1-4156-bb1b-d229591aa4e1">Wait for the cache to expire or re-run the feed</p></td></tr><tr><td colspan="1" rowspan="1"><p data-id="20beb4d3-dc6a-4f21-991f-7240239bfdea">Duplicate data source name error</p></td><td colspan="1" rowspan="1"><p data-id="c8d1ba6b-ee23-414f-854e-54b9697ca5d1">Name must be unique (case-insensitive)</p></td><td colspan="1" rowspan="1"><p data-id="b6b88d60-37df-4cb3-b82a-73d46d8d9a3f">Use a different name</p></td></tr></tbody></table>

* * *

## FAQ

**Do I need the single quotes around** `'t#...'`**?** Yes. The table is passed as a single-quoted string, e.g. `lookup('{sku}', 't#Pricing sheet')`.

**Will renaming a data source break expressions?** Possibly. The `t#<Name>` token is tied to the data source name, so after renaming you must update any expressions that referenced the old name.

**Is Key column / Value column 0-based or 1-based?** 1-based (the first column is `1`).

**A key has multiple values — which one is returned?** By default the raw value is returned (which may be an array when a key repeats). Use `arrayOption` `'first'` / `'last'` / `'array'` to control this.

* * *

_See also:_ [Add-ons](https://docs.woolytech.com/en/collections/4-add-ons)_._
