Portal Community
PropertyValue
Directive nameapi
Required isolation levelTrusted (minimum)
ProjectApi.Services
Data sourcePre-registered API domain entries; HTTP GET with tenant credentials
Path structure$api.DomainAlias.path.to.field
CachingResponse 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

ErrorCauseFix
DomainNotAllowlisted: MyApiDomain alias not registered in Expression_ApiDomainsRegister the domain via the API Domains admin UI or direct DB insert
IsolationViolation: $api requires TrustedNode isolation level is below TrustedSet node isolation to Trusted in node settings
ApiTimeout: exceeded 5000msRemote API too slowIncrease TimeoutMs on the domain registration; consider using a dedicated HTTP node instead
ApiAuthFailure: 401Credential Vault reference invalid or secret rotatedCheck AuthSecretRef in domain registration; re-rotate in Credential Vault
PathNotFound: domain.fieldAPI returned a different response shapeUse @default; validate API response contract