← All guided builds

Guided build · 07

External enrichment, safely

Combine API-backed signals and remote scoring while keeping downstream decisions computable through pending work and failures.

You will finish with: An inspectable enrichment model with eager and lazy work, named boundaries, and explicit fallback policy.

25 minIntermediateSource checked for Grid 0.61.0Reviewed 2026-08-24
Related canonical example04-external-enrichment.grid
Get Grid
Starter modelexternal-enrichment-starter.gridAuthored inputs and fallback outputs before provider calls are connected.

Watch it in Grid

See the workflow before you build it.

Follow the finished interaction, then use the written steps below to build and inspect it yourself.

Companion film

Live data in

Bring in a live FX rate, contain a failed ML signal with a deterministic fallback, and keep exposure recalculating.

19 secGrid 0.61.0
Open film page
On this page

What you will build

A portable enrichment model that combines a local currency exposure with two API-backed exchange rates and a remote model score. The external bindings remain raw and inspectable, while downstream outputs apply explicit fallback policy.

You will learn to:

  • choose eager = or lazy ~= for asynchronous work;
  • use named arguments at an external boundary;
  • keep raw provider results separate from fallback-safe calculations;
  • predict exactly what the model returns before providers respond or when they fail;
  • understand why pending, stale, and failed are different states.

Before you start

You should be comfortable with cells, ranges, functions, and THEN … ELSE.

The model validates without credentials, but live results require:

  • a Grid runtime with async workers enabled;
  • network access to the configured FX_RATE provider;
  • a configured ml.scoring route for ML_SCORE.

Grid's default FX provider is the keyless Frankfurter v2 service for daily central-bank/reference rates. A workspace can replace it with GRID_FX_RATE_BASE_URL. ML_SCORE is workspace-configured, so its scale and exact result are not portable.

1. Start with deterministic local inputs

MODEL "External Enrichment"
DESCRIPTION "Portable model that delegates async work to external workers."
VERSION "1.0.0"
AUTHOR "Grid Team"
TAGS "canonical", "portable", "external", "async"

# Inputs
A1 IS currency = 250000
A2 IS percentage = 18pct
A3 = 0.12
A4 = 0.18
A5 = 0.27
A6 = 0.43
A7 = "EUR"
A8 = "USD"

A1 is the amount to convert. A3:A6 is the feature vector sent to the scorer. A7 and A8 define the FX direction.

Checkpoint: A1 is 250,000; A3:A6 contains four numeric features; the currency pair is EUR to USD.

A2 is deliberately unused in the canonical fixture. Changing it should produce no downstream recomputation. That is useful dependency-inspector evidence, but a production model should remove or connect unused inputs.

2. Request the eager FX signals

# External signals
B1 = FX_RATE(A7 AS base, A8 AS quote)
B3 = FX_RATE("GBP", "USD")

Named arguments make the first call's direction explicit. FX_RATE(base, quote) returns quote currency per unit of base currency, so B1 is USD per EUR.

Both calls use eager = because the model's primary outputs need them immediately. Depending on timing, their status may move through dirty, queued, running, and ready too quickly to observe every state.

Live checkpoint: when the provider succeeds, B1 and B3 are provider-returned reference rates. Do not compare them with a fixed tutorial number; they vary by date and provider.

If the provider is unavailable and there is no satisfactory cached value, the raw binding fails. We will keep that failure visible and apply policy downstream.

3. Add a lazy remote score

B2 ~= ML_SCORE(A3:A6)

~= waits until something reads B2. It is appropriate for work that is expensive or not always needed.

The later C2, C3, and C5 outputs depend on B2, so reading any of them will pull the lazy score. An authoring surface that reads all visible outputs can therefore make the score appear to start immediately.

Checkpoint: before a dependent is read, B2 can remain lazy. After reading B2, C2, C3, or C5, the scorer should be requested. Its numeric result is provider-specific.

4. Convert the raw signals into fallback-safe values

# Fallback-safe analytics
C1 IS currency = ROUND(A1 * (B1 DEFAULT 1.05), 2)
C2 = B2 DEFAULT 0

DEFAULT consumes either BLANK or an error. It does not overwrite B1 or B2, so their external status and raw results remain inspectable.

With no cached result while work is pending, the binding carries BLANK and the fallback is used. After a terminal failure, the error is also consumed. If a permitted stale cached value exists, that value remains available and DEFAULT does not replace it.

Fallback checkpoint: with neither provider result available, C1 is exactly 262,500 and C2 is 0.

When FX succeeds, C1 becomes ROUND(250000 * B1, 2). When scoring succeeds, C2 becomes the configured scorer's result.

5. Derive a decision, a cross-rate, and an audit string

C3 = C2 > 0.35 THEN "manual-review" ELSE "auto-approve"
C4 = ROUND((B3 DEFAULT 1.25) / (B1 DEFAULT 1.05), 4)
C5 = `eur_usd={B1 DEFAULT 1.05} score={C2}`

C3 converts the score into a review decision. C4 divides USD-per-GBP by USD-per-EUR, yielding EUR per GBP when both rates are live. Each side has its own fallback, so one failed provider result cannot erase the calculation.

Full fallback checkpoint:

Output Expected value
C1 262,500
C2 0
C3 auto-approve
C4 1.1905
C5 eur_usd=1.05 score=0

Live C1, C2, C4, and C5 are intentionally not fixed checkpoints. They depend on provider data.

6. Test dependency isolation

First change A1 from 250000 to 300000.

Only the converted amount needs a new local calculation. The FX call's arguments did not change, so this edit does not invalidate its cache key. A normal access-driven TTL refresh can still request a newer value.

Checkpoint: on the fallback path, C1 becomes exactly 315,000. With a live rate it becomes ROUND(300000 * B1, 2).

Now change A6 from 0.43 to 0.50 and read C3.

The changed feature vector invalidates B2; reading the decision pulls a new lazy score. B1, B3, and C4 are unaffected.

Checkpoint: only the score-dependent branch is invalidated. The new score and decision remain provider-specific.

Try it yourself

The canonical fallback score of 0 leads to auto-approve. That is useful for demonstrating continuity, but it is permissive for a real risk workflow. Make an unavailable score require review:

C3 = (ISBLANK(B2) OR ISERROR(B2)) THEN "manual-review" ELSE B2 > 0.35 THEN "manual-review" ELSE "auto-approve"

This catches a new pending call with no cached value as well as a failed call. A stale cached score is still a value, so use the runtime's external-status display if your policy must reject stale results too.

Then make the audit label follow the authored currency inputs:

C5 = `{A7}_{A8}={B1 DEFAULT 1.05} score={C2}`

For an advanced extension, replace an ambient HTTP_JSON fetch with declared authority:

REQUIRES prices = NETWORK("https://prices.example.com", GET)

spot(path) = NETWORK.GET_JSON(prices, path)

D1 = spot("/spot/EURUSD")
D2 = WITH payload = D1, rate = payload.rate
THEN rate ELSE 1.05

The example origin is a placeholder. It will run only after replacing it with a real canonical HTTPS origin and receiving a matching host grant. NETWORK.GET_JSON must be the entire body of its defining function; field extraction belongs downstream.

What the fallback does—and does not—guarantee

  • Pending is not an error. A queued or running binding carries a cached value or BLANK.
  • Background refresh can serve a stale cached value while requesting a new one.
  • Refresh is access-driven; an idle model does not refresh merely because time passes.
  • External writebacks are revision-checked, so an older result cannot overwrite newer inputs.
  • Typical terminal errors include TIMEOUT and EXTERNAL; a runtime can also report stale fallback.
  • The constants 1.05, 1.25, and 0 are business assumptions, not universal safe defaults.
  • Combining one live rate with one fallback rate keeps the model computable but does not make the resulting cross-rate market-consistent.
  • Frankfurter supplies daily reference rates, not executable or real-time trading quotes.
  • A threshold of 0.35 is meaningful only if the configured scorer is calibrated for it.

You are done when

  • Raw external bindings remain inspectable.
  • The exact fallback checkpoint matches.
  • Changing A1 recomputes conversion without invalidating the scorer.
  • Changing one feature invalidates the lazy scorer without touching FX.
  • You can explain the difference between pending, stale, failed, and fallback.
  • You can identify which fallback policies must change before production use.
Build statusReached the expected checkpoint?