Getting Started with Custom Functions
Custom Functions let you run your own TypeScript inside Transcend. Your code executes on your Sombra gateway as part of a workflow, which means it can reach systems Transcend's cloud cannot, and the credentials it needs never leave your infrastructure.
This guide orients you to the Custom Functions section of the dashboard. It covers what the four surfaces do:
- Functions list
- Activity log
- New function
- Push from your code
and which one to use for what. For the runtime itself (the function arguments, environment variables, the key-value store, and OAuth information) see Custom Functions Overview .
A Custom Function is a serverless TypeScript function that Transcend triggers as part of a workflow. It runs in a Deno sandbox on your Sombra gateway, not in Transcend's cloud. See technical details in Custom Functions Overview.
Custom Functions do what a webhook integration does, without the infrastructure. There is no service to stand up, no endpoint to expose, no retry logic to write, and no monitoring to wire up. You write the code, test it, and publish it.
Two things trigger a Custom Function today:
- A Rules Automation rule. The rule fires (on a webhook or on a schedule) and runs your function as its action.
- A DSR workflow. Transcend resolves a data point against a system that has no prebuilt integration, or runs a preflight check to enrich an incoming request.
| Type | What it's for |
|---|---|
| General | Rules Automation actions, preference syncs, webhook handling: everything that isn't a DSR data system. |
| DSR | Connecting a system to DSR workflows. Needs a default export that resolves data points, plus an optional enricher export for preflight checks. |
A General function is reusable. Reference it from as many rules and integrations as you need; when you publish an update, every reference picks it up.
Warning: Type is locked once a function is linked to a data system. Changing it means disassociating the data system first, so choose deliberately.
You need two things.
A dedicated Sombra gateway. Custom Functions run on single-tenant or self-hosted Sombra only. If your organization is on a multi-tenant gateway, function execution is rejected.
API key scopes. Two scopes govern access:
- View Custom Functions: see function definitions, versions, and run history
- Manage Custom Functions: create, edit, test, publish, and archive functions
Rules Automation keys that hold Execute Rules keep the execution access they already had.
Go to Developer Tools → Custom Functions, or open app.transcend.io/infrastructure/functions () directly.
The section has two tabs, Functions and Activity, and two buttons that create functions, New function and Push from your code.
The Functions tab is the list of custom functions for your organization. The Activity tab shows all the runs of individual custom functions for your organization.

The Referenced by column on the Functions table tells you which of these a given function is wired into other custom functions, DSRs, etc.
This is your Custom Functions inventory. Every Custom Function in your organization, regardless of who wrote it or how it got here.
Search by name or description in the search bar.

Use the checkboxes to select multiple functions for a bulk action.
| Column | What it tells you |
|---|---|
| Name | The function's display name |
| Type | General or DSR |
| Description | Free text you set; N/A when empty |
| Status | Active, Inactive, or Archived |
| Owner | The people responsible for the function |
| Runs (7 days) | Execution count for the last week. Click through to Activity, filtered to this function |
| Last run | Timestamp of the most recent execution, or Never run |
| Version | The active version number, with an indicator when an unpublished draft exists |
| Referenced by | The rules and data systems using this function, or Not referenced |
| Created at | Creation date |
Status is the function's lifecycle, and it is separate from version state.
- Active — appears in the function picker for rules and integrations, and executes when triggered
- Inactive — still appears in the picker, but does not execute even when its trigger conditions are met
- Archived — hidden from the picker. Queued runs are cancelled, in-progress runs finish, and the function can be restored later
Each row has an edit icon that opens the function, plus a menu with:
- View code: read the active version without entering the editor
- Archive: soft-delete the function
Warning: A function that is referenced by a data system or by an active or paused rule cannot be archived. The archive dialog lists what is blocking it. Remove the references first. DSR functions cannot be archived at all, because doing so would break the connection strategy of the data system they serve.
Activity is the run log for every Custom Function in your organization — where you go when something failed and you need to know why, and what it was.

| Column | What it tells you |
|---|---|
| Started | When the run began |
| Run ID | Unique identifier for the run |
| Function | Which function executed |
| Status | Queued, In Progress, Success, or a failure state |
| Trigger | What caused the run — Testing, Rules automation (links to the rule), or DSR workflow (links to the request) |
| Duration | Execution time in milliseconds. Shows — while the run is queued or in progress |
Filter by function, by lifecycle state, or by typing a function name. Export to CSV downloads the filtered view. Columns controls which columns are visible.
Failed runs are attributed. A run fails either as a
- customer execution error: something in your function threw
- Transcend error
The row menu offers two things on a failed run:
- Download log: the full console output for that run, including everything your function wrote with
console.log. Previously this required Sombra access to retrieve. - Open audit trail: jumps to the audit trail filtered to this function, showing who changed what and when
Info: Run history is retained for 30 days. Export to CSV if you need a longer record.
When creating or editing a function, use the the Testing tab within the editor. You can execute your Custom Function with a test payload, which will be passed to the payload argument. Click Send Test Request to execute the Custom Function. The Custom Function will be executed in a new sandboxed process, and the log output will be shown in the console. To log output to the console, use use console.log.

You can override the test payload by changing the JSON payload in the Testing tab before clicking Send Test Request. Since events that invoke Custom Functions can also invoke webhooks, payloads are identical to their respective webhook bodies.
Environment variables can also be overridden in the test payload, and they default to the special value of ${YOUR_ENV_VAR_NAME}, which means it will be replaced with the value of the environment variable as specified in the Environment Variables tab.
Use New function when you want to author in the dashboard: prototyping, a one-off, or a function whose code does not belong in a Git repository.
New function opens a type picker. This is the choice described in Types of Custom Function (#Types-of-Custom-Function) above, and the dialog restates the constraint: it cannot be changed once the function is linked to a data system.

Choosing DSR takes you into the Integrations catalog to create a Custom Function integration first, since a DSR function has to be attached to a data system. Give the integration a title, assign an owner, choose the Sombra gateway, and click Add.
Choosing General opens the function editor directly.
If you selected General, the editor has a details panel on the left and two steps on the right.

On the left, set:
- Name (required)
- Description worth filling in, since it is a column on the list
- Owner(s) (required) — defaults to you
- Referenced by — read-only, and populates once a rule or data system points at this function
Under Configuration & Code:

Sombra. Select the gateway this function runs on. The dropdown lists your available gateways by name and URL, and you can type to filter.
Environment Variables. Key-value pairs passed to your function through the environment argument. Values are encrypted at rest and masked in logs. Put credentials here rather than in code. Variable names must start with a letter and contain only letters, numbers, and underscores; CONSTANT_CASE is the convention, and the DENO_ prefix is reserved.
Each row has two icons. The eye hides the value in the form. The trash icon removes the row. Add variable adds another.
Code. A TypeScript editor with IntelliSense for the argument types and Deno globals.

A General function starts from this template:
export default async function handler({
environment,
payload,
sdk,
}: CustomFunction.MaestroArgument): Promise<void> {
console.log('Environment variables:', environment);
console.log('Payload:', payload);
}Your changes autosave to a draft as you type. The header shows the last save time.

Click Next to move to Testing.
Set a test payload. The JSON object your function receives as payload — and click Run test. The run executes on your gateway in a fresh sandboxed process using the environment variables you configured in the previous step.
A successful run returns a green panel with a collapsible Stdout section containing everything your function logged, plus the exit code and execution time:
Custom Function ran successfully
Stdout
Environment variables: { CRM_API_TOKEN: "********" }
Payload: {
message: "hello world!",
coreIdentifier: { value: "example-identifier" }
}
Exit code: 0 · Execution time: 65ms
Secrets are masked. The environment variable logs as ********, not its value. This is the masking behavior working as intended. You can log environment while debugging without leaking credentials into the run log.
Transcend adds fields to your payload. The test above supplied only message, but the function received coreIdentifier as well. Your test payload is merged with the context Transcend supplies, so write your function against what actually arrives rather than against your test JSON alone.
Test runs are logged on the Activity tab with a Testing trigger.
Warning: You cannot publish until a test passes. Until then the editor shows Testing required. Run a successful test in the Testing step before you can publish, and the publish button stays disabled. A passing test enables it.
Click Save and activate.

Every function carries a version number, and Transcend manages it for you.
- New functions start at V1.0. Each published change increments by 0.1 — V1.0, V1.1, V1.2.
- The number is system-assigned. You cannot set it.
- Versioning covers code only. Renaming a function, editing its description, or changing owners does not create a version.
- Exactly one version is active. Publishing a draft makes it active and marks the previous version inactive.
- Draft versions never fire on a trigger. They run only when you run them from the Testing step.
Open Version history from the Code section to see every version, when it was published, and whether it came from the dashboard or was pushed via the API. Select any version to view its code read-only.
Restore copies a version's code into a new draft. It does not roll the version number backward. Restoring V1.0 while V1.3 is active creates V1.4 as a draft with V1.0's code; V1.0 stays inactive and keeps its run history intact. Review the draft, test it, and publish it like any other change.
Push from your code is the path for functions that live in a repository. Transcend never reads your repo — you push to Transcend, from your own CI.
Use it when function code should be reviewed, versioned in git, and deployed like the rest of your software.
Code will not sync from within Transcend -only into Transcend.
You define your functions in a transcend-functions.yml manifest and push them with the Transcend CLI, either locally or from a CI job.
Code and environment values are signed against your Sombra gateway over TLS before anything reaches Transcend. Only signed JWTs are stored — your code and secrets never hit Transcend's backend in plaintext.
Every push test-runs the signed code on your gateway against the test payloads in your manifest. Nothing is persisted during those runs. If any payload fails, the function is rejected and the job fails with the logs attached.
1. Create an API key with the Manage Custom Functions scope, and store it as TRANSCEND_API_KEY. Store your Sombra internal key alongside it.
2. Install the CLI:
npm i -D @transcend-io/cli3. Write a manifest. Save it as transcend-functions.yml in your repository root:
functions:
- name: Sync preferences to CRM
description: Pushes preference store changes into our CRM
code: ./functions/sync-preferences.ts
type: GENERAL
sombra-id: <<parameters.sombraId>>
test-payloads:
- payload: '{"message": "hello world!"}'
env:
- name: CRM_API_TOKEN
value: <<parameters.crmToken>>Useful fields:
| Field | Required | Notes |
|---|---|---|
name | Yes | Display name. Used as the sync key when no id is set |
code | Yes | Path to the TypeScript source, relative to the manifest |
id | No | Function ID. When set, it becomes the sync key, which lets you rename safely |
type | No | GENERAL (default) or DSR |
data-silo-id | DSR only | Omit on a new DSR function and the integration is created for you |
sombra-id | No | The gateway this function runs on |
test-payloads | No | Payloads the push must pass before the function is accepted |
timeout-ms | No | Per-function timeout |
env | No | Environment variables. Use <<parameters.name>> placeholders and supply values with --variables so secrets stay out of git |
4. Push:
transcend custom-functions push \
--auth=$TRANSCEND_API_KEY \
--sombraAuth=$SOMBRA_INTERNAL_KEYUseful flags: --dryRun to validate without writing, --promote to activate the new version, --force to push when the CLI sees no diff, --skipTests to bypass the test gate, and --updateManifest to write function IDs back into your manifest. Add --transcendUrl=https://api.us.transcend.io if you are on US hosting.
transcend custom-functions list prints the functions in your organization.
A composite action wraps the CLI. Run it with dry-run: 'true' on pull requests and with promote: 'true' on merges to your main branch:
- uses: transcend-io/custom-functions-manager@v1
with:
api-key: ${{ secrets.TRANSCEND_API_KEY }}
sombra-auth: ${{ secrets.SOMBRA_INTERNAL_KEY }}
file: ./transcend-functions.yml
promote: 'true'Pushed functions appear in the Functions list like any other, with their version history labeled Pushed via API.
Warning: A function is tied to the channel that last updated it. Once you push to a function via the CLI, its code becomes read-only in the dashboard, and further changes have to come from your codebase.
Two behaviors surprise people:
- Rotating only an environment variable's value looks like no change. Values are encrypted at sign time and cannot be diffed, so the CLI reports nothing to do. Push with
--force. - Renaming a function without a pinned
idcreates a second function. Run--updateManifestonce to write IDs into your manifest, and renames match correctly from then on.
| Constraint | Detail |
|---|---|
| Gateway | Dedicated single-tenant or self-hosted Sombra only |
| Runtime | 30 seconds per run by default. Configurable when self-hosting Sombra |
| Sandbox | Network access only. No filesystem, child processes, system information, FFI, or system environment variables |
| Isolation | Each invocation runs in a new sandboxed process. No two Transcend customers share hardware |
| Key-value store | Strings only. 128 keys, 128 characters per key, 2048 characters per value. Not shareable between functions |
| Run history | 30 days |
| Not supported | Data discovery and classification |
A Custom Function reports back to Transcend by calling the one of the custom integration API endpoints through the sdk. Branch on payload.type to handle each request type.
For an ACCESS request, upload the data subject's profile data with one of the DSR Access request endpoints (e.g., POST to /v1/data-silo). Put the retrieved data in the profileData object of each entry in the profiles array, and set status to "READY" once the data is ready for Transcend to collect.
export default async function customFunction({
environment,
payload,
sdk,
}: CustomFunction.Argument): Promise<void> {
if (payload.type !== 'ACCESS') return;
// Look up the data subject in your system by their identifier
const dataSubject = await fetch(
`https://api.example.com/v1/users/${payload.extras.profile.identifier}`,
{
headers: { Authorization: `Bearer ${environment.API_KEY}` },
},
).then((res) => res.json());
// Upload the results back to Transcend
// API Reference: https://docs.transcend.io/docs/api-reference/POST/v1/data-silo
const response = await sdk.fetch('/v1/data-silo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${environment.TRANSCEND_API_KEY}`,
},
body: JSON.stringify({
profiles: [
{
// Example body data returned
profileData: {
email: dataSubject.email,
fullName: dataSubject.fullName,
createdAt: dataSubject.createdAt,
},
},
],
status: 'READY',
}),
});
}If your system has no matching data subject, still report back so the profile resolves. A common pattern is to send an empty profileData with a status of "SKIPPED" to indicate there is no data to return.
For an ERASURE request, perform the deletion in your system, then mark the work complete with a PUT to /v1/data-silo. Only report success after the deletion has actually completed. If the deletion call fails, throw an error so the request isn't marked complete prematurely.
export default async function customFunction({
environment,
payload,
sdk,
}: CustomFunction.Argument): Promise<void> {
if (payload.type !== 'ERASURE') return;
// Delete the data subject in your system
const eraseResponse = await fetch(
`https://api.example.com/v1/users/${payload.extras.profile.identifier}`,
{
method: 'DELETE',
headers: { Authorization: `Bearer ${environment.API_KEY}` },
},
);
// Mark the erasure complete in Transcend
// API Reference: https://docs.transcend.io/docs/api-reference/PUT/v1/data-silo
const response = await sdk.fetch('/v1/data-silo', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${environment.TRANSCEND_API_KEY}`,
},
body: JSON.stringify({
status: 'RESOLVED',
}),
});
}Not all Custom Functions will apply to a single Data Action type. In these cases, you can use logic statements to determine which code to run for each relevant Data Action type using the payload.type variable.
export default async function customFunction({
environment,
payload,
sdk,
}: CustomFunction.Argument): Promise<void> {
// Custom Function general code
// Define logic for running code for either ACCESS or ERASURE requests
if (payload.type === 'ACCESS') {
// ACCESS request code, including response
} else if (payload.type === 'ERASURE') {
// ERASURE request code, including response
}
}For information on how to configure your Custom Function to perform Preflight checks, check out the Custom Function Enricher guide.