Discovery Metadata
Document your models for humans and AI discovery
Your model now defines what your data means. This page is about the next stage: optimizing it for AI retrieval. Metadata tags document your model for humans and — through the Credible AI Analytics Engine — make it discoverable to AI agents: when you publish, your tags are indexed and become searchable by people browsing models and by agents answering natural-language questions.
Two tags do the work: #(doc) describes what a field means, and #(index) makes a dimension's values searchable. A third lever, discovery curation, narrows which models and sources agents see in the first place. This page covers what to tag, index, and curate; for how indexes are built, kept fresh, and paid for, see Performance & Cost.
You don't have to write tags by hand: the agent documents as it builds — every field it defines gets its #(doc) and #(index) tags as part of the modeling workflow — and you can ask it to document an existing model the same way. Review its work with this page in mind; good documentation directly improves the quality of chat-based analysis downstream.
Documentation Tags
Use #(doc) tags to add human-readable descriptions to fields. Place the tag on the line before the field definition.
source: orders is conn.table('sales.orders') extend {
dimension:
#(doc) The unique identifier for each order
order_id is id
#(doc) The date when the order was placed
order_date is created_at::date
#(doc) Customer segment based on lifetime value: High (>$10k), Medium (>$1k), Low
customer_segment is case
when lifetime_value > 10000 then 'High'
when lifetime_value > 1000 then 'Medium'
else 'Low'
end
measure:
#(doc) Total revenue from all orders
total_revenue is sum(amount)
#(doc) Number of orders that have shipped
shipped_orders is count() { where: status = 'shipped' }
}The best #(doc) descriptions carry meaning the code can't: units, business rules, thresholds, and caveats — like the segment cutoffs above — not restatements of the field name.
Value Indexing
Use #(index) to index all distinct values from a column. This enables AI agents to find fields by searching for data values, not just field names or descriptions.
source: products is conn.table('catalog.products') extend {
dimension:
#(index)
#(doc) The product name
product_name is name
#(index)
#(doc) The product category (e.g., Electronics, Clothing, Home & Garden)
category is product_category
#(index)
#(doc) The brand name
brand is brand_name
}Why Value Indexing Matters
Consider a product catalog where the column is named product_category but contains values like "Running Shoes" and "Athletic Apparel". When a user asks "What are our top-selling sports gear products?", field names and documentation alone won't surface those products — nothing is named "sports gear".
With value indexing, the Credible AI Analytics Engine searches the actual values: it matches the question to fields whose values fit, then helps the agent construct a query with the correct filter — finding "Running Shoes" and "Athletic Apparel" without exact string matching.
When to Use Value Indexing
Use #(index) on:
- Names and titles - Product names, customer names, program titles
- Categorical values - Status codes, types, categories
- Lookup values - Region names, department names, brand names
Skip value indexing for:
- Numeric fields (amounts, counts, IDs)
- Timestamps and dates
Example: Well-Documented Model
source: orders is conn.table('sales.orders') extend {
primary_key: order_id
join_one: customers is conn.table('sales.customers') on customer_id = customers.id
join_one: products is conn.table('catalog.products') on product_id = products.id
dimension:
#(doc) Date the order was placed
order_date is created_at::date
#(doc) Order status: pending, processing, shipped, delivered, cancelled
#(index)
status is order_status
#(doc) Product category from the catalog
#(index)
category is products.product_category
#(doc) Customer's geographic region
#(index)
region is customers.region
measure:
#(doc) Total number of orders
order_count is count()
#(doc) Total revenue in USD
total_revenue is sum(amount)
#(doc) Average revenue per order in USD
avg_order_value is total_revenue / order_count
#(doc) Percentage of orders that were cancelled
cancellation_rate is count() { where: status = 'cancelled' } / order_count * 100
view:
#(doc) Monthly revenue trend with order counts
monthly_revenue is {
group_by: order_date.month
aggregate: total_revenue, order_count
}
}Curating Discovery
Tags improve how well agents understand what they find; discovery curation narrows what they find in the first place. By default, every model is listed and every source is directly queryable. Curation happens at two levels: export in the model itself controls which sources a file exposes, and manifest fields control which files agents land on.
export { … } — in the model
export { … } filters which sources within a file are exposed. A file with no export exposes all its top-level sources; add one to keep imported or scaffolding sources out of discovery.
// Intermediate building block — hidden from discovery
source: raw_order_lines is conn.table('sales.order_lines') extend {
measure: line_count is count()
}
// The curated interface built on it — this is what agents should find
source: orders is raw_order_lines extend {
measure: order_revenue is sum(amount)
}
export { orders }With the export, agents discover orders but not raw_order_lines; without it, both would be listed.
explores and queryableSources — in the manifest
Add optional fields to the package manifest (publisher.json, described in Publishing) to point agents at the model files that matter:
{
"name": "ecommerce",
"version": "0.0.1",
"description": "ecommerce demo data",
"explores": ["orders.malloy", "customer_health.malloy"],
"queryableSources": "declared"
}explores — .malloy file paths (relative to the package root) whose models agents should discover and land on.
- Omit — discovery unchanged; every model and source stays listed.
- Set — listings narrow to these files, plus each file's
export { … }closure.
queryableSources — whether non-curated sources can still be queried by name. Only applies when explores is set.
| Value | Listing | Query by name |
|---|---|---|
"declared" (default) | Curated set only | Non-curated sources return 404 |
"all" | Curated set only | All sources still queryable |
This curates discovery, not access — what is listed and queryable by name, not who may query. To restrict access by user or group, use #(authorize) and secure givens, covered on the next page.
Hiding an #(authorize)-gated source. An access gate blocks querying, not discovery, so a gated source still appears in listings to callers who can't query it. To hide it while keeping it queryable for authorized callers, leave it out of the curated explores/export set and set queryableSources: "all". Under the default "declared", the non-curated source returns 404 by name — which blocks authorized callers too.