---
title: "Feed query language"
source: https://support.storeconnect.com/articles/feed-query-language
type: article
format: markdown
site: StoreConnect Support — product and developer documentation for StoreConnect
site_index: https://storeconnect.com/llms.txt
docs_index: https://support.storeconnect.com/llms.txt
note: Append .md to any page or article URL on this site to get its Markdown form.
---
# Feed query language

A feed's records come from a query: a base object plus four optional clauses. The clauses use SQL syntax but are not SQL. They are parsed, checked against the fields that actually exist, and rebuilt before they reach the database, so nothing written here is passed through as a SQL string.

| Part | Field on **Query** | Purpose |
|---|---|---|
| Object | `s_c__Object_API_Name__c` | The base object to select from, as a Salesforce API name such as `product2` or `s_c__article__c` |
| `from` | `s_c__From__c` | Declares related objects to join, each with an alias and a match condition |
| `where` | `s_c__Where__c` | Filters which records are returned |
| `order` | `s_c__Order__c` | Orders them |
| `select` | `s_c__Select__c` | Optional. Projects a specific set of columns instead of whole records |

A query with only an object returns every record of that object.

Each part is a separate field on the **Query** record (`s_c__Query__c`), not one statement typed in one place. A field holds its clause body without the leading keyword, so **Where** holds `s_c__active__c = true`, not `WHERE s_c__active__c = true`. **From** is the exception: it holds the join clauses themselves, each beginning with `JOIN`, `INNER JOIN` or `LEFT JOIN`. Every example below is the contents of one field.

## Naming fields

Refer to a field by its Salesforce API name in full, including prefixes and suffixes: `s_c__slug__c`, `name`, `createddate`. Shortened or Ruby-style names are not accepted.

An unqualified name is a field on the base object. A qualified name such as `brand.name` is a field on a join, where `brand` is an alias declared in `from`. Keywords like `AND` and `JOIN` are case-insensitive.

An unknown object, unknown field or undeclared alias is rejected loudly. A query is never quietly reduced to an empty result because of a typo.

## `where`

| Operator | Meaning |
|---|---|
| `=` `!=` `>` `>=` `<` `<=` | Comparisons |
| `BETWEEN x AND y` | Inclusive range |
| `IN (a, b)` / `NOT IN (a, b)` | Membership |
| `LIKE 'pattern'` / `NOT LIKE 'pattern'` | Pattern match, case-sensitive |
| `ILIKE 'pattern'` / `NOT ILIKE 'pattern'` | Pattern match, case-insensitive |
| `IS NULL` / `IS NOT NULL` | Null checks |

Combine with `AND`, `OR`, `NOT` and parentheses. Values are single-quoted strings (doubling a quote to escape it), numbers, `true` or `false`, and `NULL`.

```sql

s_c__is_master__c = true AND s_c__active__c = true

s_c__price__c BETWEEN 10 AND 50 AND brand.name LIKE 'Ac%'

type = 'physical' AND (s_c__category__c IN ('sale', 'clearance') OR s_c__featured__c = true)
```

:::warning
A negated comparison does not match null. `!=`, `NOT IN` and `NOT LIKE` leave out rows where the field is empty, which is rarely what the author intended. Add `IS NULL` explicitly when those rows should be included.
:::

### Matching regardless of case

Every comparison is case-sensitive by default, so `name = 'Red'` does not match `red`.

For pattern matching, use `ILIKE` instead of `LIKE`. For everything else, wrap both operands in `LOWER()` or `UPPER()`:

```sql

LOWER(s_c__brand__c) = LOWER('Acme')

LOWER(s_c__category__c) IN (LOWER('Sale'), LOWER('Clearance'))
```

Folding one side only leaves the other side's case intact and still fails to match, which is the usual mistake. `LOWER` and `UPPER` need a text column and are rejected on a number, date or boolean.

## `from`

```sql

[INNER | LEFT] JOIN <object> AS <alias> ON <col> = <col> [AND <col> = <col>]
```

Joins default to `INNER`. Use `LEFT` to keep base rows with no match, such as a product with no brand. The `ON` is a match key only, written out in full, and joins on equality; it is not a second filter. An unqualified column in the `ON` is on the base object.

Several joins are allowed, and a later one may reference an earlier alias to form a chain, which is how a junction object is traversed:

```sql

JOIN account AS brand ON s_c__brand_id__c = brand.sfid

JOIN s_c__products_product_categories__c AS pc ON sfid = pc.s_c__product_id__c
JOIN s_c__product_category__c AS cat ON pc.s_c__category_id__c = cat.sfid
```

Aliases must be unique and must not share a name with a base-object field.

### To-one and to-many joins behave differently

Which one a join is comes from the `ON`: the side whose matched column is that table's own `sfid` is the "one" side.

- **To-one** (`base.<foreign key> = alias.sfid`) matches at most one related row, and can be used in both `where` and `order`.
- **To-many** (`base.sfid = alias.<foreign key>`) may match many. It can filter in `where`, compiled as an existence check so base rows are never duplicated, but ordering by it is rejected, because ordering by a one-to-many relationship has no defined answer.

## `order`

```sql

<field> [ASC | DESC] [NULLS FIRST | NULLS LAST] [, ...]
```

Order by a base field or a to-one join field. Ordering by a to-many join field is rejected. To sort regardless of case, wrap the field in `LOWER()`.

## Bindings

A clause can read a live value with a `:path` binding, evaluated against the same data a feed template sees. This is what lets one query serve every store:

```sql

s_c__store_id__c = :current_store.id
```

A binding may only appear where a value goes: the right side of a comparison, inside `IN`, or as a `BETWEEN` bound. It can never stand in for a field, an alias, a `LIKE` pattern or an ordering term. The value is bound as a parameter rather than pasted into the statement, so a binding cannot be an injection route.

A binding is a plain path. No filters, no expressions. It must resolve to a single value; one that resolves to an object, to nothing, or to a list fails loudly, as does a request-specific value used by a pre-built feed, which renders with no request.

### Two reserved bindings

| Binding | Resolves to |
|---|---|
| `:last_built_at` | When this store's feed was last pre-built. A feed never built binds the epoch, so its first build is a full one |
| `:now` | The time the feed renders |

These make an incremental feed possible:

```sql

systemmodstamp > :last_built_at
```

Each build records its own start time, so a record changed while a build was running is picked up by the next one. On a live request `:last_built_at` is the epoch, which makes the feed a full one.

## `select`

Without a `select`, a feed returns whole records. Adding one switches to projection: the feed's records become exactly the columns listed, each addressed in the template by its alias.

```sql

sfid AS id,
name AS item_name,
brand.s_c__display_name__c AS brand,
STRING_AGG(cat.sfid, '|') AS group_ids,
MIN(pbe.unitprice) AS price
```

The template shape does not change; only which fields exist:


```liquid

{% for record in record_set.records %}
  {{ record.id }} - {{ record.item_name }} ({{ record.group_ids }})
{% endfor %}
```


You can project a base column, a to-one joined column, or an aggregate over a to-many join. A bare to-many column outside an aggregate is not projectable.

### Aggregates

An aggregate reduces a to-many collection to one value per base record, such as joining every category id a product belongs to.

`COUNT`, `SUM`, `MIN`, `MAX`, `AVG`, `STRING_AGG` and `JSON_AGG` are available, and `DISTINCT` is allowed inside any of them.

- `STRING_AGG` requires a delimiter, and the others must not be given one.
- `COUNT(*)` is not supported. Name the to-many alias, as in `COUNT(cat.sfid)`, so it is unambiguous which collection is being counted.
- Aggregating a base or to-one column is rejected, because there is no collection to aggregate.
- Aggregates over different collections compose without inflating each other's counts.
- `JSON_AGG` projects a real array. In a JSON feed, printing it directly encodes it as a JSON array, and a record with no matches yields `[]` rather than nothing.

### Aliases

A bare base field takes its own name as its alias. Anything else, a qualified column or any aggregate, requires an explicit `AS`, because a column with no alias cannot be addressed in the template. Aliases must be unique, and are lower-cased, so reference them in lower case whatever you wrote.

:::warning
A to-one join used only in `select` and never in `where` is still an `INNER` join, so base rows whose foreign key is empty are dropped from the feed. Declare it as `LEFT JOIN` to keep them, and the projected column comes through empty.
:::

---

## Follow StoreConnect

- [Email Newsletter](https://storeconnect.com/c/lp-newsletter)
- [LinkedIn Newsletter](https://www.linkedin.com/build-relation/newsletter-follow?entityUrn=7444956928444862464)
- [YouTube](https://www.youtube.com/channel/UCngKdP2x8l1wcbAKW3tvU8g)
- [LinkedIn](https://www.linkedin.com/company/storeconnect)
- [X / Twitter](https://x.com/storeconnecthq)

## Popular Links

- [Partners](https://storeconnect.com/partners)
- [Become a Partner](https://storeconnect.com/become-a-partner)
- [News](https://storeconnect.com/articles/news)
- [Events](https://storeconnect.com/articles/events)
- [Feature Comparison](https://storeconnect.com/how-we-compare)
- [Download a free trial](https://appexchange.salesforce.com/appxListingDetail?listingId=a0N3A00000FMkeKUAT)
- [Book a Demo](https://storeconnect.com/contact)

## Documentation

- [Help documentation](https://support.storeconnect.com/help-documentation)
- [AI agents](https://support.storeconnect.com/ai)
- [Videos & tutorials](https://support.storeconnect.com/videos-tutorials)
- [Developer reference](https://support.storeconnect.com/developer-reference)
- [Release notes](https://support.storeconnect.com/release-notes)
- [Troubleshooting](https://support.storeconnect.com/troubleshooting)
- [Trust Center](https://trust.getstoreconnect.com/)
- [Status Page](https://status.storeconnect.com/)

## Contact

- info@getstoreconnect.com
- US +1 415 745 3230
- AUS +61 2 8365 2308

100 S Ashley Dr, Suite 600-2461
Tampa FL 33602-600 USA

Level 22, Sydney Place
180 George Street
Sydney, NSW, 2000, AUS

## Machine-readable

- [Site index for agents](https://storeconnect.com/llms.txt): curated map of the StoreConnect site in llms.txt format
- [Documentation index for agents](https://support.storeconnect.com/llms.txt): full technical and product documentation map

Every page and article on this site has a Markdown rendering: append `.md` to its URL.

Continue in Markdown: [Help documentation](https://support.storeconnect.com/help-documentation.md) · [Developer reference](https://support.storeconnect.com/developer-reference.md) · [Videos & tutorials](https://support.storeconnect.com/videos-tutorials.md) · [Release notes](https://support.storeconnect.com/release-notes.md)

---

StoreConnect Support — https://support.storeconnect.com/articles/feed-query-language