External functions

External Functions

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 ttlMs but within maxStalenessMs, 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: 0 means 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:

  1. A stale cached value may be used if one is available and the function allows stale fallback.
  2. 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:

  • https scheme 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