$api — External API Directive
Fetches data from a pre-registered external API endpoint at expression evaluation time. Results are cached and path-navigated identically to other directives.
| Property | Value |
|---|---|
| Directive name | api |
| Required isolation level | Trusted (minimum) |
| Project | Api.Services |
| Data source | Pre-registered API domain entries; HTTP GET with tenant credentials |
| Path structure | $api.DomainAlias.path.to.field |
| Caching | Response cached per domain+path within the execution (or longer with @cache-process) |
Allowlist enforcement
Only API domains registered in the
Expression_ApiDomains table are callable. Attempting to call an unregistered domain raises DomainNotAllowlisted. This prevents workflow authors from exfiltrating data to arbitrary endpoints.
Syntax
Read a field from a registered API domain
{@ $api.CrmLookup.customer.email }
With query parameters embedded in path
{@ $api.ExchangeRates.rates.EUR @cache-process }
Entire response as JSON
{@ $api.ProductCatalog.items @json }
API Domain Registration Schema
-- Expression_ApiDomains: pre-registered callable APIs
CREATE TABLE Expression_ApiDomains (
ID INT IDENTITY PRIMARY KEY,
TenantID INT NOT NULL,
Alias NVARCHAR(100) NOT NULL, -- used as first path segment
BaseUrl NVARCHAR(500) NOT NULL, -- e.g. https://api.crm.com/v2
AuthType NVARCHAR(50), -- Bearer | ApiKey | Basic | None
AuthSecretRef NVARCHAR(200), -- Credential Vault ref
TimeoutMs INT DEFAULT 5000,
RateLimitPerMin INT DEFAULT 60,
IsActive BIT DEFAULT 1,
CreatedOn DATETIME NOT NULL,
CreatedBy INT NOT NULL,
-- ... standard audit columns
)
Complex Scenarios
Scenario 1 — Real-Time Exchange Rate Lookup
Fetch the current EUR→GBP rate from a registered FX provider at expression time:
Amount converted to GBP using live rate
{@ $js`
const rate = parseFloat(context.api.FxProvider.rates.GBP);
const amount = parseFloat(context.input.current.amountEur);
return (amount * rate).toFixed(2);
` }
Calls FxProvider API once; result cached for the execution lifetime
Scenario 2 — CRM Customer Lookup During Workflow
Enrich an incoming invoice with customer data from the CRM without a separate HTTP node:
// Domain: "CrmApi" → base https://crm.acme.com/v2/customers/{customerId}
// Path navigation: api.CrmApi resolves the base URL + customerId from input
{
"customerId": "{@ $input.current.customerId }",
"creditLimit": {@ $api.CrmApi.creditLimit },
"accountStatus": "{@ $api.CrmApi.status }",
"tier": "{@ $api.CrmApi.tier @default:standard }"
}
Scenario 3 — Address Validation Service
Validate and standardise an address using an address-lookup API inline in an expression:
{@ $js`
// context.api.AddressLookup returns the validated address response
const addr = context.api.AddressLookup;
if (!addr || addr.confidence < 0.8) {
throw new Error('Address validation failed: ' + (addr?.reason || 'low confidence'));
}
return JSON.stringify({
line1: addr.formatted.line1,
city: addr.formatted.city,
postcode: addr.formatted.postcode,
country: addr.formatted.countryCode
});
` }
Scenario 4 — Product Price Catalogue Lookup
Read a live price from a product catalogue API, falling back to the input price if the API is unavailable:
Unit price — live catalogue with input fallback
{@ $api.ProductCatalogue.products.unitPrice @default:{@ $input.current.listedPrice } }
Falls back to listedPrice from input if the catalogue API path is unavailable
Scenario 5 — Tax Jurisdiction Rate from Government API
Fetch the current VAT rate for a specific tax jurisdiction from a government-registered API:
{@ $js`
// GovTaxApi registered for https://api.revenue.gov.ie/v1/rates
const jurisdiction = context.input.current.vatJurisdiction; // e.g. "IE_STD"
const rateData = context.api.GovTaxApi.rates[jurisdiction];
if (!rateData) throw new Error('Unknown jurisdiction: ' + jurisdiction);
return rateData.rate; // 0.23
` }
// Combined with @cache-process — rate fetched once per process lifecycle
Scenario 6 — Multi-Step Enrichment Chain
Use two API domains sequentially: first resolve a customer tier, then look up the discount rate for that tier:
// VariableAssignment node 1: store tier from CRM
VariableName: "customerTier"
Value: {@ $api.CrmApi.tier @uppercase } → "GOLD"
// VariableAssignment node 2: look up discount rate for that tier
VariableName: "discountRate"
Value: {@ $api.PricingApi.tiers.GOLD.discountRate @default:0 } → 0.15
// Final calculation node:
{@ $js`
const price = context.input.current.listPrice;
const discount = parseFloat(context.vars.discountRate);
return (price * (1 - discount)).toFixed(2);
` }
Common Errors
| Error | Cause | Fix |
|---|---|---|
DomainNotAllowlisted: MyApi | Domain alias not registered in Expression_ApiDomains | Register the domain via the API Domains admin UI or direct DB insert |
IsolationViolation: $api requires Trusted | Node isolation level is below Trusted | Set node isolation to Trusted in node settings |
ApiTimeout: exceeded 5000ms | Remote API too slow | Increase TimeoutMs on the domain registration; consider using a dedicated HTTP node instead |
ApiAuthFailure: 401 | Credential Vault reference invalid or secret rotated | Check AuthSecretRef in domain registration; re-rotate in Credential Vault |
PathNotFound: domain.field | API returned a different response shape | Use @default; validate API response contract |