External Functions
External functions fetch data, call AI, query databases, or run work that may not finish immediately. Their results are written back into your model asynchronously.
External Functions
External functions fetch data, call AI, query databases, or run work that may not finish immediately. Their results are written back into your model asynchronously.
Use external functions when a formula depends on something outside the workbook: market data, HTTP JSON, model scoring, AI prompts, or a bounded database query.
1. Built-In External Functions
| Function | Returns | Common use |
|---|---|---|
FX_RATE(base, quote) |
fx_rate number |
Currency conversion |
HTTP_JSON(url) |
object | Fetch JSON from an HTTP endpoint |
ML_SCORE(features) |
score number |
Score a feature vector with a configured model |
PREDICT(modelid, features) |
value | Point prediction from a trained model |
PREDICT_PROBA(modelid, features) |
object | Class probabilities from a trained classifier |
PREDICTION_BOUND(modelid, features, probability, direction) |
number | Calibrated one-sided predictive bound from a capable regression model |
MODEL_UNCERTAINTY(modelid) |
object | Predictive-uncertainty capability and calibration evidence |
AI_PROMPT(prompt, options) |
ai_text string |
Generate text from a prompt |
ASK(question, context, options) |
ai_answer string |
Ask a question over context |
ASK_TEXT(question, context, options) |
ai_answer string |
Ask for a text answer |
ASK_NUMERIC(question, context, options) |
ai_number number |
Ask for a numeric answer |
ASK_BOOLEAN(question, context, options) |
ai_boolean boolean |
Ask for a boolean answer |
ASK_JSON(question, context, options) |
ai_json object |
Ask for structured JSON |
PG_SELECT(table, query) |
postgres_query_result |
Read bounded PostgreSQL results |
REDIS_QUERY(connection, query, context) |
redis_query_result |
Read an allow-listed Redis value with argument and response caps |
REDIS_MUTATE(connection, mutation, context) |
redis_mutation_result |
Run a fingerprinted, result-replaying idempotent Redis mutation |
Your Grid workspace may expose additional model-backed functions such as
CHURN_SCORE(features). Treat them like any other external function: add a
fallback and make downstream formulas resilient while the value is pending.
Schema-backed APIs can add namespaced functions without extending the built-in catalog:
USE "api:example/catalog@1.0.0" AS catalog
items = catalog.listItems(limit: 100)
The imported package fixes the operation ID, argument names, result shape, and package digest. Only query operations are callable from formulas; effect, publish, and subscribe operations require an explicit action surface.
Calibrated model bounds
PREDICTION_BOUND is provider-neutral. A model must publish a versioned
predictive-uncertainty capability before the call is admitted. The probability
selects an exact admitted one-sided coverage level in the open interval
(0, 1). The direction argument is extensible, but the current XGBoost
producer admits "upper" only:
forecast = PREDICT("weekly-demand@2", A2:C2)
upper_p90 = PREDICTION_BOUND("weekly-demand@2", A2:C2, 0.9, "upper")
uncertainty = MODEL_UNCERTAINTY("weekly-demand@2")
Use the exact model@version reference returned by training when evaluating a
new candidate. An unversioned model id resolves the pinned version, which may
still be an older point-only model.
An upper P90 means the calibration method targets 90% marginal predictive
coverage across comparable future rows. It is not a promise that every
individual row has 90% conditional coverage. Inspect MODEL_UNCERTAINTY for
the method, split, row counts, observed held-out coverage, supported
probabilities/directions, limitations, and unavailable reason.
This contract intentionally keeps PREDICT unchanged. A one-sided calibrated
bound is not a full predictive distribution, so Grid does not expose
PREDICT_QUANTILE or manufacture a lower bound, interval, variance, or standard
deviation from upper-P90 data. Unsupported models, probabilities, directions,
and task types fail closed instead of returning the point prediction.
Predictive bounds are also separate from the workbook CONF_* / PROB_*
verbs. Those verbs propagate authored workbook input distributions; they do not
replay external model inference to synthesize trained-model uncertainty.
For training, artifact registration, exact-version selection, and unavailable calibration handling, follow Train and Use Calibrated Predictions.
2. Calling An External Function
External functions are called like ordinary functions:
A1 = FX_RATE("EUR", "USD")
A2 = HTTP_JSON("https://api.example.com/widgets/42")
A3 = ML_SCORE([0.1, 0.2, 0.3, 0.4])
A4 = AI_PROMPT("Summarize this note", { temperature: 0.2 })
A5 = ASK_NUMERIC("What is the forecasted revenue?", B1:B12, { temperature: 0 } AS options)
A6 = SELECT id, amount FROM finance.public.orders WHERE amount >= 100 ORDER BY id DESC LIMIT 25
A7 = REDIS_QUERY("default", { operation: "get", key: "pricing:current" }, {})
Named arguments work too:
A1 = FX_RATE("GBP" AS base, "USD" AS quote)
The binding eventually contains the external result. Until then, it moves through a defined status lifecycle.
3. Status Lifecycle
A binding that depends on an external function can be:
| Status | Meaning |
|---|---|
dirty |
Needs recomputation; no work has started yet |
queued |
Waiting to be processed |
running |
Work is in progress |
ready |
A current value is available |
stale |
An older value is available while a refresh is wanted |
failed |
The latest attempt failed and no satisfactory value is available |
You usually do not need to branch on these statuses inside formulas. Instead, write formulas with fallbacks so downstream bindings remain computable.
4. Eager Vs Lazy Calls
Both eager and lazy assignments can call external functions.
Eager =
rate = FX_RATE("EUR", "USD")
Use eager = when the value is central to the model and should be requested as
soon as the model runs.
Lazy ~=
score ~= ML_SCORE([0.1, 0.2, 0.3, 0.4])
Use lazy ~= when the value is expensive or rarely viewed. The work starts when
something reads the binding.
5. Always Add A Fallback
External calls are network- and provider-dependent. Pair them with DEFAULT,
IFERROR, or WITH ... ELSE:
rate = FX_RATE("EUR", "USD")
price_usd = ROUND(price_eur * (rate DEFAULT 1.08), 2)
For longer chains:
converted = WITH rate = FX_RATE("EUR", "USD"), price = base * rate
THEN ROUND(price, 2) ELSE 0
If any step fails, WITH returns the ELSE value.
6. Nested External Calls
You can nest an external call inside a larger formula:
A1 = ROUND(FX_RATE("EUR", "USD") + 0.01, 4)
Grid tracks the external call separately from the wrapping expression. That lets the fetched value be cached and retried independently while the surrounding formula stays ordinary spreadsheet logic.
For clarity, prefer naming the boundary explicitly when the value is reused:
eur_usd = FX_RATE("EUR", "USD") DEFAULT 1.08
quoted_rate = ROUND(eur_usd + 0.01, 4)
7. Caching, TTL, And Refresh
Each external function declares a cache policy:
| Field | Meaning |
|---|---|
ttlMs |
Intended time-to-live for a fetched value |
maxStalenessMs |
Hard staleness bound after which a value should be refreshed before use |
refreshMode |
"blocking" or "background" refresh behavior |
Typical behavior:
- Within
ttlMs, the cached value is served as current. - Past
ttlMsbut withinmaxStalenessMs, background refresh may serve the cached value while asking for a new one. - At or past
maxStalenessMs, the value is treated as expired and refreshed before it is served. ttlMs: 0means the value does not age out on its own.
Refresh is access-driven: a value is checked when the binding, or a dependent binding, is read. A fully idle model does not refresh just because wall-clock time passed.
8. Failure Handling
If an external call fails:
- A stale cached value may be used if one is available and the function allows stale fallback.
- Otherwise the binding reports an error value.
Detect failures with ordinary error tools:
ISERROR(rate)
rate IS ERROR
price = rate DEFAULT 1.08
Your model should never depend on an external value being immediately available.
9. Capability Requirements (REQUIRES)
Legacy fetchers like HTTP_JSON run with whatever ambient connector access the
workspace grants them. Capability requirements are the declared-authority
alternative: the module states exactly which network origin or secret purpose
it needs, and the host grants (or refuses) that exact authority at admission.
A requirement is a declaration of need, never a grant — a module with no
matching host grant fails closed and queues no work.
Grant configuration is currently a programmatic host-integration capability, not a public permission-setting API or an automatic consequence of importing a model. Deployments without configured grants deny this path. Extension permissions and ordinary connector credentials do not themselves grant it.
Declaring Requirements
Requirements are declared at module scope, one per line:
REQUIRES <alias> = NETWORK("<https-origin>", GET)
REQUIRES <alias> = SECRET("<logical.purpose>")
REQUIRES crm = NETWORK("https://api.example.com", GET)
REQUIRES crm_auth = SECRET("crm.read")
The alias binds a named capability selector — an origin plus method set, or a
secret purpose — in a distinct authority-only namespace. It is not a
value: it cannot be stored in a cell, concatenated into a string, captured
by a closure, passed to FUNCTION, or serialized. It is legal only in the
authority positions of the NETWORK.GET_JSON intrinsic described below.
Alias names are canonicalized case-insensitively to uppercase ASCII, so crm
and CRM are the same requirement. Declaration order does not matter: a
formula may use an alias before the REQUIRES line that declares it.
NETWORK Origins And Methods
The NETWORK origin string must be a canonical HTTPS origin:
httpsscheme only;- a DNS host name, not an IP literal (the host is stored lowercase and IDNA-normalized);
- the default HTTPS port only;
- no userinfo, path, query, or fragment;
- at most 512 bytes.
The method must be exactly GET (any capitalization). Other methods,
redirects, non-default ports, and private-network addresses are rejected —
the worker re-verifies all of this at transport time.
SECRET Purposes
SECRET("crm.read") names a logical purpose, never a credential. The
purpose must be 1–256 bytes. Host policy resolves the purpose to a concrete
credential binding after admission; the model, compiled artifact, cache keys,
diagnostics, and reflection retain only the purpose string. Rotating the
underlying credential changes the binding version, so data fetched under old
authority is never reused.
Consuming Requirements: NETWORK.GET_JSON
NETWORK.GET_JSON is a compiler-owned intrinsic (it is not in the workbook
function catalog):
NETWORK.GET_JSON(network-requirement, relative-path [, secret-requirement])
The relative path is an ordinary string expression and cannot change the scheme, authority, or port fixed by the requirement. The optional third argument attaches the secret purpose's credential at the final transport boundary.
REQUIRES crm = NETWORK("https://api.example.com", GET)
REQUIRES crm_auth = SECRET("crm.read")
customer(id) = NETWORK.GET_JSON(crm, "/customers/" & id, crm_auth)
A1 = customer("42")
Without a secret:
REQUIRES prices = NETWORK("https://prices.example.com", GET)
spot(symbol) = NETWORK.GET_JSON(prices, "/spot/" & symbol)
B1 = spot("EURUSD")
A capability call behaves like any other external function: it is asynchronous, cached, retried, and deduplicated, and the fetched JSON becomes the whole target value. The usual advice applies — keep downstream formulas resilient while the value is pending or failed (section 8).
One structural restriction is stricter than ordinary external calls: the
capability call must be the entire body of its defining function (or a
tail call to another such function). Composing around it in the same body —
INDEX(NETWORK.GET_JSON(...), "name"), branching between two capability
calls, or storing the pending result — is rejected at compile time. Bind the
whole direct call to an addressed cell, as in A1 = customer("42"), and
compose downstream from that cell. A bare named-value binding such as
result = customer("42") is not an admitted target in this first path.
Capability-bearing definitions also cannot currently be imported from .gs
libraries because cross-module requirement rebinding is not implemented.
HTTP_JSON remains source-compatible and does not automatically acquire
capability guarantees; use REQUIRES + NETWORK.GET_JSON when you want
declared, exactly-scoped authority.
Requirements And Decision Packages
DECISION PACKAGE declarations carry their own CAPABILITIES (...) list.
Those are package-contract capability tokens matched exactly against the
native package contract at model load — a separate concept from REQUIRES
aliases, which feed the network/secret authority path. Both are declarations
that the host admits fail-closed, but they occupy different namespaces and
never reference each other. See
decision-packages.md.
Diagnostics
Parse-time diagnostics:
| Code | Trigger |
|---|---|
GRID_REQUIREMENT_KIND |
The kind after = is not NETWORK or SECRET |
GRID_REQUIREMENT_NETWORK_METHOD |
The NETWORK method is anything other than GET |
GRID_REQUIREMENT_NETWORK_ORIGIN |
The origin fails canonical-HTTPS validation: wrong scheme, IP literal or missing DNS host, non-default port, userinfo, path/query/fragment, or over 512 bytes |
GRID_REQUIREMENT_SELECTOR |
The selector fails core validation, e.g. a SECRET purpose that is empty or over 256 bytes |
GRID_REQUIREMENT_DUPLICATE |
The same alias (ASCII-case-insensitive) is declared more than once |
Module-finalization diagnostics:
| Code | Trigger |
|---|---|
GRID_REQUIREMENT_COLLISION |
The alias collides with another namespace: a named value, callable binding or parameter, Extension or API binding, imported value, module import alias, CHOICE declaration, or the reserved NETWORK intrinsic namespace |
GRID_REQUIREMENT_VALUE_USE |
The alias is used as a value outside its NETWORK.GET_JSON authority position |
GRID_REQUIREMENT_LIMIT |
The module declares more than 1,024 requirements |
GRID_REQUIREMENT_INVALID |
A collected requirement fails canonical validation during callable finalization (a fail-closed backstop for non-parser construction paths) |
10. Worked Example
MODEL "External Enrichment"
VERSION "1.0.0"
# Inputs
A1 is currency = 250000
A2 = "EUR"
A3 = "USD"
# External values
B1 = FX_RATE(A2 AS base, A3 AS quote) DEFAULT 1.08
B2 ~= ML_SCORE([0.12, 0.18, 0.27, 0.43]) DEFAULT 0
# Fallback-safe analytics
C1 is currency = ROUND(A1 * B1, 2)
C2 = B2 > 0.35 THEN "manual-review" ELSE "auto-approve"
END MODEL
B1 requests a rate as soon as the model runs. B2 waits until something reads
it. Both provide fallback values so C1 and C2 stay useful.
11. Restrictions
| Constraint | Notes |
|---|---|
| External calls cannot supply a rule assignment RHS | Bind value-producing calls at the top level or in DO |
| A bare external call in a rule is an effect | Grid commits rule state first, then enqueues the call with a distinct firing identity |
External calls in DO follow formula lifecycle |
Dependency staging is deterministic, but recalculation/cache invalidation may revisit the call; use a rule action for a durable per-firing mutation |
| Schema-backed effects cannot run as ordinary formulas | Table mutations use an explicit top-level command or a post-commit rule action |
| External calls may time out or be rate-limited | Always provide a fallback |
| External writebacks are revision-checked | Older results cannot overwrite newer inputs |
12. See Also
assignments.md—~=operator details.errors.md— handling failed external calls.rules-and-schedules.md— atomic rule commits and post-commit effects.decision-packages.md— package-contractCAPABILITIESlists, distinct fromREQUIRESaliases.