Skip to main content
CQL (Context Query Language) is a short string that names which entities belong to a scope. You write a CQL string anywhere Penumbra asks for a scope: to preview context, to author a slice, or to compile context.
type:Company shape:"Account Intelligence" after:7d
That reads as: Company entities, in the Account Intelligence shape, created in the last 7 days. No UUIDs, no SQL, no embeddings — just names.

How CQL works

The mental model is the most important thing to hold:
A scope is a set, and CQL describes its membership. It answers “what is in this slice?” — not “how do I rank or search it?” Ranking and traversal are separate concerns (see What CQL is not).
A CQL string is a sequence of prefix:value tokens (plus optional date bounds). Each token names a dimension of membership:
TokenMeansExample
type:Entity type nametype:Company
shape:Shape display name (one per slice)shape:"Account Intelligence"
source:Source file namesource:"Q2 Deck.pdf"
source-shape:Entities from a source and a shapesource-shape:"Q2 Deck.pdf"->"Account Intelligence"
after: / before:Created-at boundsafter:7d before:2026-01-01
Two rules govern how tokens combine:
  • Same kind → union (OR). type:Company type:Person means Company or Person.
  • Different kind → intersection (AND). type:Company source:"Q2 Deck.pdf" means Company entities that also come from Q2 Deck.pdf.
Values are names, not IDs. Quote any value with spaces: shape:"Account Intelligence". Penumbra resolves names to the underlying ids for you, and tells you (via a warning) if a name doesn’t resolve.

Dates: absolute or relative

after: and before: bound the created-at timestamp. They accept an absolute ISO date or a relative expression, resolved server-side in UTC:
FormMeaning
2026-01-01absolute ISO date
last-week, last-month, last-yeara week / month / year ago
today, yesterday, nowstart of today / yesterday / this instant
7d -7d 2w 12h 3mo 1yN units ago (the sign is ignored — scopes look at the past)
type:Company after:last-month     # Companies from the last month
type:Insight after:7d             # Insights from the last 7 days

Write your first CQL string

Build a scope one token at a time, previewing as you go so you see exactly what it matches before you save it:
# 1. Start broad
type:Company                       # every Company

# 2. Add a shape (intersect)
type:Company shape:"Account Intelligence"

# 3. Add a time bound
type:Company shape:"Account Intelligence" after:30d

# 4. Widen the type (union)
type:Company type:Person after:30d
Preview returns a matched count and any warnings, so each step gives immediate feedback:
const { matched, scope_warnings } = await pb.context.preview({
  cql: "type:Company after:7d",
});
// matched: 4, scope_warnings: []

CQL string vs. structured filter

Every scope can be written two ways. They express the same thing; pick by feel.
CQL string (cql)Structured filter (filter)
ShapeOne terse lineA JSON object
Best forHumans, quick scoping, the canonical formProgrammatic building, property predicates
Property predicates✗ (not expressible){ path, operator, value }
gt / lt operators
// These two are equivalent:
{ cql: 'type:Company shape:"Account Intelligence" after:2026-01-01' }

{ filter: {
    types: ["Company"],
    shapes: ["Account Intelligence"],
    after: "2026-01-01",
} }
Property predicates have no CQL syntax — reach for the structured filter:
{ filter: {
    types: ["Company"],
    properties: [{ path: "stage", operator: "eq", value: "alpha" }],
} }

What CQL is not

CQL scopes membership. It is deliberately not a search or ranking language, so a few things you might try are not part of a slice — and Penumbra tells you so in scope_warnings rather than silently dropping them:
  • Free text (renewal risk) — that’s a search concern. Use search, then scope the results.
  • edge: and agent: — traversal and provenance, not membership.
  • sort: — ordering is applied when you read, not when you define the set.
  • A second shape: — a slice resolves one shape; the rest are ignored with a warning.
This “honest warnings” behavior is intentional: a scope that under-resolves tells you why, so you never get a silent matched: 0.

Where to go next

  • Context and slices — preview, author, and compile scopes
  • Planes — the layers CQL scopes across
  • Search — when you need ranking and free text, not membership