Skip to content

Custom Adapters

Custom adapters are JavaScript functions that run in sandboxed V8 isolates. Use them when the declarative mapping DSL isn’t enough — XML parsing, HMAC signatures, complex business logic, etc.

An adapter is a JavaScript function that transforms data. Five adapter types cover different stages of the pipeline:

TypePurposeExample
request_transformTransform outbound request payloadConvert JSON to XML
response_transformTransform inbound responseParse XML response to JSON
authSign or authenticate requestsAdd HMAC signature header
enrichmentEnrich source data before mappingFetch additional data, compute fields
fullFull request/response lifecycleComplete custom protocol handling

Generate adapter code from a natural language description:

Terminal window
fyrn adapters generate "Convert XML order to JSON format"
fyrn adapters generate "Add HMAC signature header" --type auth
fyrn adapters generate "Flatten nested address fields" --sample '{"address":{"street":"123 Main"}}'

The AI generates JavaScript code, runs it against sample input (if provided), and shows the results. You review, name, and save.

{
"name": "create_adapter",
"arguments": {
"name": "shopify-order-transform",
"description": "Transform Shopify order webhook payload to NetSuite SalesOrder format",
"adapter_type": "request_transform",
"sample_input": {
"id": 12345,
"line_items": [{"sku": "WIDGET-A", "quantity": 2, "price": "10.00"}]
}
}
}

Provide JavaScript code directly:

{
"name": "create_adapter",
"arguments": {
"name": "add-timestamp",
"description": "Adds a processed_at timestamp to every record",
"adapter_type": "enrichment",
"code": "export default function transform(input) { return { ...input, processed_at: new Date().toISOString() }; }"
}
}
export default function transform(input) {
// input: the payload object
// return: the transformed payload
return {
...input,
processed_at: new Date().toISOString()
};
}

The following helper modules are injected into the isolate and available to your adapter code:

HelperImportDescription
xmlimport { parse, build } from 'fyrn:xml'Parse XML strings to JSON objects and build XML from JSON. Based on fast-xml-parser.
cryptoimport { hmac, hash, uuid } from 'fyrn:crypto'HMAC-SHA256/SHA512, SHA-256/SHA-512 hashing, and UUID v4 generation.
base64import { encode, decode } from 'fyrn:base64'Base64 encode/decode strings and binary data.
csvimport { parse, stringify } from 'fyrn:csv'Parse CSV strings to arrays of objects and stringify back.
dateimport { format, parse, diff } from 'fyrn:date'Date formatting (ISO 8601, Unix timestamps), parsing, and diff computation.

Standard JavaScript globals are available: JSON, Date, Math, Map, Set, Array, Object, String, RegExp, Promise, parseInt, parseFloat, encodeURIComponent, decodeURIComponent, TextEncoder, TextDecoder.

Not available: fetch, XMLHttpRequest, require, process, eval, Function constructor, setTimeout, setInterval. Adapters are pure data transforms — side effects are handled by the runtime.


Adapters run in isolated V8 isolates (via isolated-vm):

Security constraints:

  • No network access. Adapters cannot make HTTP requests, open sockets, or communicate externally. All I/O is handled by the fyrn runtime outside the isolate.
  • No filesystem access. No fs, path, or any file system APIs. Adapters operate only on the data passed in.
  • Memory limit: 128 MB. The isolate is terminated if it exceeds this threshold. This prevents unbounded allocations from affecting other flows.
  • Execution timeout: 5 seconds. Long-running or infinite loops are killed. Adapter logic should be fast — if you need heavy computation, break it into smaller steps.
  • No dynamic code execution. eval(), new Function(), and import() are disabled. All code must be statically defined.
  • No ambient globals. process, globalThis.fetch, require, and Node.js built-ins are not available.

What is injected:

The runtime injects the fyrn:* helper modules (see above) and the standard JavaScript built-ins (JSON, Date, Math, etc.). Your adapter receives a plain object as input and must return a plain object — no classes, streams, or callbacks.


Terminal window
fyrn adapters test <adapter-id> --input '{"order_id": "12345"}'
{
"name": "test_adapter",
"arguments": {
"adapter_id": "...",
"input": {"order_id": "ORD-123", "line_items": [{"sku": "A", "qty": 2}]}
}
}

Test results show the output payload and any errors.


Terminal window
# List all adapters
fyrn adapters list
# View pre-built templates
fyrn adapters templates
{
"name": "update_adapter",
"arguments": {
"adapter_id": "...",
"code": "export default function transform(input) { return { ...input, version: 2 }; }",
"description": "Updated to add version field"
}
}

import { parse } from 'fyrn:xml';
export default function transform(input) {
// input.body contains the raw XML string from the source system
const parsed = parse(input.body, {
ignoreAttributes: false, // preserve XML attributes
attributeNamePrefix: '@_', // prefix attributes with @_
});
// Extract order data from the parsed XML structure
const order = parsed.OrderResponse.Order;
return {
order_id: order.OrderID,
status: order['@_status'],
items: Array.isArray(order.LineItem)
? order.LineItem.map(li => ({
sku: li.SKU,
quantity: Number(li.Quantity),
price: Number(li.UnitPrice),
}))
: [{
sku: order.LineItem.SKU,
quantity: Number(order.LineItem.Quantity),
price: Number(order.LineItem.UnitPrice),
}],
};
}
import { hmac } from 'fyrn:crypto';
export default function transform(input) {
const { headers, body, secrets } = input;
// Compute HMAC-SHA256 over the raw request body
const timestamp = new Date().toISOString();
const payload = `${timestamp}.${JSON.stringify(body)}`;
const signature = hmac('sha256', secrets.signing_key, payload);
return {
...input,
headers: {
...headers,
'X-Signature': signature,
'X-Timestamp': timestamp,
},
};
}

The secrets object is injected by the runtime from the flow’s credential store. Secrets are never logged or persisted in adapter output.

export default function transform(input) {
const { line_items, customer } = input;
// Compute order totals
const subtotal = line_items.reduce(
(sum, item) => sum + item.quantity * item.unit_price, 0
);
const tax = subtotal * 0.08;
const total = subtotal + tax;
// Derive customer tier from lifetime spend
const tier = customer.lifetime_spend > 10000 ? 'enterprise'
: customer.lifetime_spend > 1000 ? 'professional'
: 'starter';
return {
...input,
subtotal: Number(subtotal.toFixed(2)),
tax: Number(tax.toFixed(2)),
total: Number(total.toFixed(2)),
customer: {
...customer,
computed_tier: tier,
},
};
}