> For the complete documentation index, see [llms.txt](https://swu-forge.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://swu-forge.gitbook.io/docs/cards-and-search/searching-cards/search-syntax.md).

# Forge Syntax Reference

Forge Syntax lets you find cards using structured **function-call** filter expressions like `aspect(red) cost(>4)`. It is available to **all users** as an alternative to Plain Language Search (which requires a Foundry Apprentice subscription).

> **Two dialects.** SWU Forge also supports [Scryfall Syntax](/docs/cards-and-search/searching-cards/scryfall-syntax.md) — a prefix style (`c:r p>4`) familiar from Scryfall/SWUDB. The search box **auto-detects** which you're typing and remembers your choice; you can also set it from the search-mode dropdown on the Browse page or from [Settings](/docs/account/settings.md). This page documents the **Forge** dialect, which is a superset of the card-property filters.

## How It Works

Type filter expressions directly into the search bar. Filters use the format `filterName(value)`. Any text that is not part of a filter expression is treated as a **card name search**.

Multiple filters combine with **AND** logic -- a card must match every filter to appear in results.

Syntax is always taken **literally** — even with Plain Language Search turned on. `aspect(red)` narrows results to cards with Aggression and nothing else; Forge never adds conditions you didn't type, such as ruling out the other five aspects. Only plain-language wording ("red cards") is open to interpretation.

## Recipes (quick start)

Copy any of these into the search bar to get a feel for the syntax. Each is explained in the [Filter Reference](#filter-reference) below.

| Query                                              | Finds                                                |
| -------------------------------------------------- | ---------------------------------------------------- |
| `power(>cost) type(unit)`                          | Units that hit above their cost                      |
| `hp(>power) type(unit)`                            | Tanky units (more HP than power)                     |
| `power(>=cost+2) hp(>=cost+2) type(unit)`          | Overstatted units — both stats at least 2 above cost |
| `cost(>=2, <=5, !=3)`                              | Costs 2–5, but never exactly 3                       |
| `aspect(only(blue, black))`                        | Cards playable in a Vigilance/Villainy deck          |
| `aspect(exactly(red))`                             | Mono-Aggression only                                 |
| `aspect(more(red))`                                | Multi-aspect cards that include red                  |
| `aspect(count(3))`                                 | Triple-aspect cards (`count(0)` = colourless)        |
| `aspect(doubled(blue))`                            | Cards printing Vigilance twice                       |
| `upgradePower(>=2)`                                | Upgrades/pilots granting +2 power or more            |
| `pilotingCost(<=1) trait(pilot)`                   | Cheap-to-deploy pilots                               |
| `number(196)`                                      | Whatever card is #196 in each loaded set             |
| `vanilla() type(unit)`                             | Plain units with no ability text                     |
| `or(aspect(red), aspect(blue))`                    | Red **or** blue                                      |
| `type(unit) or(aspect(green), aspect(villainy))`   | Units that are green **or** villainy                 |
| `or(keyword(sentinel), keyword(ambush)) cost(<=3)` | Cheap Sentinel or Ambush cards                       |
| `aspect(only(red)) trait(trooper) cost(<=2)`       | Cheap mono-red Troopers                              |

## Filter Reference

### aspect()

Filter by card aspects. Supports several sub-modes and bare values:

* `any(...)` -- card has **at least one** of the listed aspects (OR)
* `all(...)` -- card has **all** of the listed aspects, and may have others (AND / superset)
* `not(...)` -- card does **not** have any of the listed aspects (exclude)
* `exactly(...)` -- card has **exactly** these aspects, no more and no fewer
* `only(...)` -- card's aspects are a **subset** of the listed set (nothing off-colour); colourless cards still match -- "playable in"
* `more(...)` -- card has **all** the listed aspects **plus at least one other**
* `count(n)` -- card has a specific **number** of distinct aspects; accepts comparisons like `count(>=2)` or `count(0)` for colourless
* `doubled(...)` -- card prints the listed aspect **twice** (e.g. double Vigilance)
* Bare values (no sub-mode) are treated as `any`

**Aspect aliases:**

| Aspect     | Aliases                        |
| ---------- | ------------------------------ |
| Aggression | `aggression`, `red`            |
| Vigilance  | `vigilance`, `blue`            |
| Command    | `command`, `green`             |
| Cunning    | `cunning`, `yellow`            |
| Heroism    | `heroism`, `white`, `hero`     |
| Villainy   | `villainy`, `black`, `villain` |

**Examples:**

* `aspect(red)` -- cards with the Aggression aspect
* `aspect(any(red, blue))` -- cards with Aggression or Vigilance
* `aspect(all(green, hero))` -- cards with both Command and Heroism (may also have others)
* `aspect(exactly(red, black))` -- cards that are exactly Aggression + Villainy, nothing else
* `aspect(only(blue, black))` -- cards playable in a Vigilance/Villainy deck (no off-colour aspects)
* `aspect(more(red))` -- multi-aspect cards that include Aggression
* `aspect(count(2))` -- dual-aspect cards; `aspect(count(0))` -- colourless cards
* `aspect(doubled(blue))` -- cards printing Vigilance twice
* `aspect(not(villain), any(red, blue))` -- Aggression or Vigilance cards, excluding Villainy

### cost()

Filter by card cost. Accepts exact numbers and comparison expressions using the operators `=`, `!=`, `<`, `<=`, `>`, `>=`.

Multiple expressions separated by commas use **AND** logic -- a card must satisfy **all** conditions. This is how you define ranges.

**Examples:**

* `cost(3)` -- cards that cost exactly 3
* `cost(>4)` -- cards that cost more than 4
* `cost(<=2)` -- cards that cost 2 or less
* `cost(!=3)` -- cards that cost anything except 3
* `cost(>=3, <=6)` -- cards that cost between 3 and 6 (AND: must be >= 3 **and** <= 6)
* `cost(>=2, <=8, !=5)` -- cards costing 2-8 but not exactly 5

> **AND vs OR:** `cost(>=3, <=6)` means "3 through 6" because both conditions must be true. If you want cards that are either cheap **or** expensive (OR logic), use two separate cost filters or exact values -- e.g., `cost(1, 2, 7, 8)` for exact costs.

#### Comparing stats to each other

Instead of a number, the operand can be another stat field -- `cost`, `power`, or `hp` -- to compare two of a card's own stats. This works inside `cost()`, `power()`, and `hp()`.

* `power(>cost)` -- units whose power is greater than their cost
* `cost(<power)` -- same result, written from the cost side
* `hp(!=power)` -- cards whose HP differs from their power
* `power(>=cost, <hp)` -- power at least the cost **and** less than the HP

#### Doing math with stats

The stat you compare against can be a small sum: whole numbers and `cost`, `power`, `hp`, joined with `+`, `-`, and `*`. Multiplication happens before addition and subtraction, as usual. Spaces are optional.

* `power(>cost+1)` -- power at least 2 more than the cost
* `hp(>= cost + 2)` -- HP at least 2 more than the cost
* `power(>=2*cost)` -- power at least double the cost
* `power(>=2*cost-hp)` -- power plus HP adds up to at least double the cost

To compare a total like power plus HP, move one of the stats to the other side, as in the last example. Parentheses aren't supported, and you can't multiply two stats together (`cost*power`). If the math doesn't make sense, the search bar tells you instead of quietly ignoring it.

The same math works in the **Cost**, **Power**, and **HP** boxes in the filter rail -- type `> cost + 2` in the HP box to get the same cards as `hp(>cost+2)`.

### power()

Filter by card power. Same syntax and operators as `cost()` (including `!=` and stat-to-stat comparisons). Multiple expressions use AND logic. Values can be negative, and `power()` also matches the power an Upgrade or Pilot **grants** -- so `power(-2)` finds upgrades that give -2 power.

**Examples:**

* `power(5)` -- cards with exactly 5 power
* `power(3, 5)` -- cards with power 3 **or** 5 (a list of exact values is OR, like `cost()`)
* `power(>=4)` -- cards with 4 or more power
* `power(!=0)` -- cards with any nonzero power
* `power(>=3, <=5)` -- cards with power between 3 and 5
* `power(-2) type(upgrade)` -- upgrades that grant -2 power

### hp()

Filter by card HP. Same syntax and operators as `cost()`. Multiple expressions use AND logic. Values can be negative, and `hp()` also matches the HP an Upgrade or Pilot **grants** -- so `hp(<0)` finds upgrades that reduce HP.

**Examples:**

* `hp(>6)` -- cards with more than 6 HP
* `hp(1)` -- cards with exactly 1 HP
* `hp(2, 4)` -- cards with 2 **or** 4 HP
* `hp(>=4, <=7)` -- cards with HP between 4 and 7
* `hp(<0) type(upgrade)` -- upgrades that reduce HP

### upgradePower() / upgradeHp()

Filter by the **bonus power / HP an Upgrade or Pilot grants** to the unit it attaches to (the `upgradePower` / `upgradeHp` values on Upgrade cards and Pilot Units/Leaders). Only cards that actually have an upgrade bonus are matched.

Accepts exact numbers and the comparison operators `=`, `<`, `<=`, `>`, `>=`. Multiple expressions use AND logic to form ranges. (`!=` is not supported here.)

**Examples:**

* `upgradePower(>=2)` -- upgrades/pilots granting 2 or more power
* `upgradeHp(3)` -- upgrades/pilots granting exactly 3 HP
* `upgradePower(>1, <=4)` -- power bonus between 2 and 4
* `upgradePower(>=2) type(upgrade)` -- Upgrade cards (excluding pilots) with a +2 or better power bonus

### pilotingCost()

Filter by a **Pilot card's piloting cost** -- the resource cost to deploy it as an upgrade onto a Vehicle. Only cards that have a piloting cost match. Accepts exact numbers and the operators `=`, `<`, `<=`, `>`, `>=` (ranges via commas). `!=` is not supported here.

**Examples:**

* `pilotingCost(2)` -- pilots with a piloting cost of exactly 2
* `pilotingCost(<=1)` -- cheap-to-deploy pilots
* `pilotingCost(>=2, <=4)` -- piloting cost between 2 and 4
* `pilotingCost(0) trait(pilot)` -- free-to-deploy pilots

### number()

Filter by **card number** -- a card's base collector number, an alternate number (for cards reprinted across products), or any of its variant collector numbers (hyperspace, showcase, prestige, and the foil / serialized variants). Accepts one or more exact numbers (comma-separated); comparisons are not supported.

Because variant numbers are reused across sets, a number can match one card in **each loaded set** (and a single card may match on more than one of its printings). Pair with `set(...)` to pin down a single printing.

**Examples:**

* `number(196)` -- the card printed as #196 (in each loaded set)
* `number(1, 2, 3)` -- cards numbered 1, 2, or 3
* `number(269) set(SOR)` -- the SOR card whose hyperspace printing is #269
* `number(84) set(SOR)` -- SOR card #84

### trait()

Filter by card traits. Comma-separated, case-insensitive. Traits are matched by closest match (prefix and substring matching), so partial names may work.

Supports three sub-modes (same as `aspect()`), plus bare values:

* `any(...)` -- card has **at least one** of the listed traits (OR — bare default)
* `all(...)` -- card has **all** of the listed traits (AND)
* `not(...)` -- card does **not** have any of the listed traits (exclude)

The literal word `and` between values acts as a separator: `trait(rebel and trooper)` is equivalent to `trait(all(rebel, trooper))`.

**Examples:**

* `trait(rebel)` -- cards with the Rebel trait
* `trait(jedi, force)` -- cards with the Jedi OR Force trait (bare = OR)
* `trait(all(jedi, force))` -- cards with both Jedi AND Force
* `trait(rebel and trooper)` -- cards with both Rebel AND Trooper
* `trait(not(droid))` -- cards without the Droid trait
* `trait(any(rebel, imperial), not(droid))` -- Rebel or Imperial cards that aren't Droids

**Bases carry the planet they're on.** Every base has a trait naming its location, so you can search by world rather than by card name -- `Echo Base` is the title, `Hoth` is the trait. Wrap names with a space in quotes.

* `trait(hoth)` -- everything from Hoth
* `trait(coruscant) type(base)` -- bases on Coruscant
* `trait("cloud city")` -- multi-word locations need quotes
* `trait(any(endor, kashyyyk))` -- bases from either forest world

An apostrophe inside a word is just part of the word, so `trait(Twi'lek) set(HMW)` works without quotes.

Aside from Naboo, which also appears on characters and ships, these names turn up on bases only.

### keyword()

Filter by card keywords. Comma-separated, case-insensitive. Like traits, keywords support closest-match resolution and the same `any(...)`, `all(...)`, `not(...)` sub-modes.

The literal word `and` between values acts as a separator: `keyword(ambush and overwhelm)` is equivalent to `keyword(all(ambush, overwhelm))`.

**Examples:**

* `keyword(sentinel)` -- cards with the Sentinel keyword
* `keyword(ambush, overwhelm)` -- cards with Ambush OR Overwhelm
* `keyword(all(ambush, overwhelm))` -- cards with both Ambush AND Overwhelm
* `keyword(ambush and overwhelm)` -- same as the previous example
* `keyword(not(restore))` -- cards without Restore
* `keyword(all(sentinel), not(restore))` -- Sentinel cards without Restore

### type()

Filter by card type. Accepted values: `unit`, `event`, `upgrade`, `leader`, `base`, `token`.

The `token` value expands to all token types (Token Unit, Token Upgrade, Credit Token, Force Token).

**Examples:**

* `type(unit)` -- all Unit cards
* `type(leader, base)` -- Leader and Base cards
* `type(token)` -- all token cards

Wrap values in `not(...)` to exclude a type instead:

* `type(not(leader))` -- everything except Leaders
* `type(unit, not(token))` -- Units, but not token Units

### arena()

Filter by arena. Accepted values: `space`, `ground`. `not(...)` excludes an arena.

**Examples:**

* `arena(space)` -- Space arena cards
* `arena(ground)` -- Ground arena cards
* `arena(not(space))` -- everything that isn't a Space card

### set()

Filter by card set. Accepts set codes or full set names (case-insensitive).

| Code | Set Name                 |
| ---- | ------------------------ |
| SOR  | Spark of Rebellion       |
| SHD  | Shadows of the Galaxy    |
| TWI  | Twilight of the Republic |
| JTL  | Jump to Lightspeed       |
| LOF  | Legends of the Force     |
| SEC  | Secrets of Power         |
| LAW  | A Lawless Time           |
| IBH  | Intro Battle: Hoth       |
| ASH  | Ashes of the Empire      |
| HMW  | Homeworlds               |
| TS26 | 2026 Twin Suns           |

Wrap a set in `not(...)` to leave it out instead. This narrows whatever you're already searching, so `set(not(HMW))` in Premier means "the Premier sets except Homeworlds" rather than every set that isn't Homeworlds.

**Examples:**

* `set(SOR)` -- cards from Spark of Rebellion
* `set(jtl, shd)` -- cards from Jump to Lightspeed or Shadows of the Galaxy
* `set(shadows of the galaxy)` -- same as `set(SHD)`
* `set(not(HMW))` -- everything except Homeworlds
* `set(jtl, lof, not(IBH))` -- Jump to Lightspeed or Legends of the Force, minus Intro Battle: Hoth

### rarity()

Filter by card rarity. Accepted values: `common`, `uncommon`, `rare`, `legendary`, `special` (or `starter`).

**Examples:**

* `rarity(legendary)` -- Legendary cards
* `rarity(common, uncommon)` -- Common or Uncommon cards
* `rarity(special)` -- Special/Starter cards

### ability()

Filter by card ability type. Supports quoted strings for multi-word values, and the same `any(...)`, `all(...)`, `not(...)` sub-modes as trait/keyword/aspect.

Accepted values:

* `when played`
* `on attack`
* `when attack ends`
* `completes attack`
* `epic action`
* `action`
* `when defeated`
* `while`
* `when deployed`

**Examples:**

* `ability(action)` -- cards with an Action ability
* `ability("when played")` -- cards with a When Played ability
* `ability("on attack", "when defeated")` -- cards with On Attack OR When Defeated
* `ability(all("when played", "on attack"))` -- cards with both When Played AND On Attack
* `ability(not("when defeated"))` -- cards without a When Defeated ability

### text()

Match a phrase against the card's **rules text** (the card's ability text, Epic Action text, and deploy box text). Card **name** and **subtitle** are not searched — use free text for name searches.

The entire value is treated as a single phrase. Wrapping quotes are optional and are stripped, so `text(friendly units)` and `text("friendly units")` are equivalent. Matching is case-insensitive and per-sentence (a phrase must appear within a single sentence of the card text).

**Examples:**

* `text(friendly units)` -- cards whose text mentions "friendly units"
* `text("draw a card")` -- cards whose text mentions "draw a card"
* `text(indirect damage) aspect(red)` -- Aggression cards that deal with indirect damage

**Searching for several words at once.** A phrase has to appear as written, so `text(defeat upgrade)` only finds cards that say "defeat upgrade" (or "defeat an upgrade"). To find cards that mention two things anywhere in their text, search for each one separately:

* `text(all(defeat, upgrade))` -- cards whose text mentions **both** "defeat" and "upgrade", anywhere on the card -- including R5-D4's "Defeat all upgrades"
* `text(defeat) text(upgrade)` -- the same thing, written as two searches (`and` between them is optional)
* `text(any(defeat, return))` -- cards whose text mentions "defeat" **or** "return"
* `text(defeat) or text(return)` -- the same idea, written with `or` between the two searches

Wrap the whole value in `not(...)` to **exclude** cards whose text matches:

* `text(not(non-leader))` -- cards whose text does **not** mention "non-leader"
* `text(not(non-leader, friendly))` -- cards whose text mentions **neither** phrase
* `text(defeat) text(not(non-leader))` -- defeat effects that aren't limited to non-leaders

`all(...)`, `any(...)` and `not(...)` only count when they wrap the entire value. Inside them, a comma separates the words you're searching for -- put a phrase in quotes if it contains a comma: `text(all("when played, draw", upgrade))`.

### title()

Match against the card's **name**. Bare text matches anywhere in the name; `starts(...)` matches only the beginning.

**Examples:**

* `title(luke)` -- cards with "luke" anywhere in the name
* `title(starts(the))` -- cards whose name begins with "the"

Plain [free text](#free-text-card-name-search) does the same thing as bare `title()`, so reach for this when you want the name search stated explicitly alongside other filters.

### rotation()

Filter by **rotation block** -- the groupings that determine Premier legality. Accepts `A`, `B`, or both.

**Examples:**

* `rotation(B)` -- cards from rotation block B only
* `rotation(A, B)` -- cards from either block

### legal()

Filter by format legality. Accepted values: `premier`, `eternal`.

**Examples:**

* `legal(premier)` -- cards legal in the Premier format

### prestige() / showcase()

Filter for cards that have a **prestige** (premium foil) or **showcase** (alternate art) variant available.

* `prestige()` -- cards with a prestige variant; `prestige(false)` for cards without one
* `showcase() set(jtl)` -- Jump to Lightspeed cards with showcase art

### suspended()

Filter by **suspended** (banned) status in the active format.

* `suspended()` -- cards currently suspended
* `suspended(false)` -- exclude suspended cards
* `suspended() legal(premier)` -- cards suspended in Premier

### vanilla()

Filter for **vanilla** cards -- cards with **no ability text at all** (no rules text, Epic Action, or deploy box). Since keyword lines are part of a card's text, keyworded cards (e.g. a Sentinel unit) are **not** vanilla.

* Bare `vanilla()` (or `vanilla(true)`) matches vanilla cards.
* `vanilla(false)` (or `vanilla(no)`) matches cards that **do** have ability text.

**Examples:**

* `vanilla()` -- all cards with no ability text
* `vanilla() type(unit) aspect(red)` -- plain Aggression units with no text
* `vanilla(false) power(>=6)` -- big-power cards that have some ability text

## Free Text (Card Name Search)

Any text outside of filter expressions is used as a **card name search**. This lets you combine name searching with filters.

**Examples:**

* `luke type(unit)` -- Unit cards with "luke" in the name
* `darth cost(<=4) aspect(black)` -- Villainy cards costing 4 or less with "darth" in the name

## Combining Filters

You can use multiple filters in a single query. Different filter types combine with AND logic -- a card must satisfy all filters to appear.

```
aspect(not(villain), any(red)) cost(>=3, <=5) type(unit) arena(ground) trait(rebel)
```

This finds Ground Unit cards that cost 3-5, have the Aggression aspect, do not have Villainy, and have the Rebel trait.

Writing **`and`** between filters is allowed and changes nothing -- `trait(jedi) and or(trait(force), trait(sith))` finds the same cards as leaving it out. It's only read as a connector when there's something to connect: in a plain name search like `luke and vader`, "and" is part of the name you're searching for.

## OR / Alternatives — or()

Everything above combines with **AND**. To match cards that satisfy **any** of several alternatives, wrap them in `or(...)`. Each comma-separated argument is its own **sub-query** -- a full mini-search that can contain its own filters and free text (which combine with AND inside that branch). A card matches the `or()` if it satisfies **at least one** branch.

**Examples:**

* `or(aspect(red), aspect(blue))` -- cards that are Aggression **or** Vigilance
* `or(hp(>3) cost(>3), luke)` -- cards with both HP > 3 **and** cost > 3, **or** any card named "luke"
* `or(keyword(sentinel), keyword(ambush))` -- cards with Sentinel or Ambush (equivalent to `keyword(sentinel, ambush)`)

**Combining `or()` with outside filters.** Filters written outside the `or()` combine with it using AND, so you can require a group of alternatives *and* another condition:

* `type(unit) or(aspect(green), aspect(villainy))` -- Units that are Command **or** Villainy
* `trait(rebel) or(cost(<=2), power(>=5))` -- Rebels that are either cheap (cost <= 2) **or** hard-hitting (power >= 5)

Notes:

* Commas **inside** a branch's own filters aren't branch separators -- `or(aspect(red, blue), type(unit))` has two branches, the first being `aspect(red, blue)`.
* `or()` does not nest, matching how the rest of the syntax works; keep alternatives in a single flat `or(...)`.

**Writing `or` between filters.** You can also type **`or`** between filters instead of wrapping them in `or(...)`. Everything to its left is one alternative and everything to its right is the other, so `type(unit) text(defeat) or text(upgrade)` finds units that mention "defeat", **or** any card that mentions "upgrade". As with `and`, a plain name search like `this or that` keeps "or" as part of the name.

## Validation

The search input validates your query as you type:

* **Unknown values** produce an error with a suggestion when a close match exists (e.g., `trait(reb)` suggests "Rebel")
* **Unknown filter names** are flagged (e.g., `damage(3)` shows "Unknown filter: damage")
* **Unclosed parentheses** are detected and reported
* **Mixing the two search styles** is caught: if you drop a prefix-style filter (like `health:>=3`) into a Forge query, it won't be applied — you'll get an error telling you the term won't work and how to write it in Forge syntax (here, `hp(>=3)`)
* When errors are present, the search input shows a **red border** and the error message appears below

Click the **?** button next to the search bar for a quick reference of all available filters and their syntax.
