# Author Custom Functions from your repository (/docs/custom-functions-overview/author-from-repository)

Author Custom Functions in your own repository and push them to Transcend with the CLI or the GitHub Action. Transcend never reads your repo. After the first push, the function appears on the **Custom Functions** page, and later code changes come from your codebase.

This page is the developer guide. For the dashboard inventory, versioning, and testing UI, see [Getting Started with Custom Functions](./custom-function-usecase-guide.mdx). For the Deno runtime, environment variables, the key-value store, and OAuth recipes, see [Custom Functions Technical Overview](./introduction-to-custom-functions.mdx).

## When to use this path [#when-to-use-this-path]

Use **Push from your code** when function code should be reviewed, versioned in git, and deployed like the rest of your software. Use **New function** in the dashboard for a prototype or a one-off that does not belong in a repository.

<Callout type="info" title="Push only">
  Code syncs into Transcend only. Edits you make in the dashboard do not write back to your repository.
</Callout>

## Prerequisites [#prerequisites]

- A dedicated <GlossaryTerm id="sombra">Sombra</GlossaryTerm> gateway. Custom Functions run on single-tenant or self-hosted Sombra only.
- An API key with **Manage Custom Functions**. Create it under **Developer Tools** → **API Keys**. Store it as `TRANSCEND_API_KEY`. This scope includes View Custom Functions.
- When your gateway requires additional authentication, store its Sombra internal key as `SOMBRA_INTERNAL_KEY`. Pass it with the `--sombraAuth` flag. The GitHub Action currently requires this input. Environment-variable values are encrypted by Sombra. Source code is integrity-protected inside a signed token, but remains readable by Transcend's backend for validation. Transcend does not access your repository.

## 1. Ask your agent [#1-ask-your-agent]

A coding agent can inspect your repository, run the CLI's deterministic initializer, and adapt the generated files to your existing conventions. Give it this prompt:

> Use the Transcend Custom Functions skill ([https://github.com/transcend-io/tools/blob/main/skills/transcend-custom-functions/SKILL.md](https://github.com/transcend-io/tools/blob/main/skills/transcend-custom-functions/SKILL.md)) to set up Custom Functions in this repository.

## 2. Via CLI [#2-via-cli]

With the Transcend CLI installed, run the interactive initializer:

```shellscript
transcend custom-functions init
```

`init` creates or reuses the default `transcend/custom-functions/transcend-functions.yml` manifest and, by default, offers Deno configuration, target-scoped VS Code settings, the Custom Function coding-agent skill, and credential-free GitHub Actions checks. It previews one transactional plan before writing and requires no Transcend credentials. After setup, use `transcend custom-functions new` to create a function and `transcend custom-functions check` to validate the project.

## 3. Manual Setup [#3-manual-setup]

If you prefer not to run `init`, assemble the repository setup using the sections below.

### Write the function [#write-the-function]

Every export receives a single argument. Destructure the fields you use. A General function looks like this:

```typescript
import type { CustomFunction } from '@transcend-io/custom-function-types';

// deno-lint-ignore require-await
export default async function handler({
  payload,
}: CustomFunction.GeneralArgument): Promise<void> {
  console.info('Payload keys:', Object.keys(payload));
}
```

A data subject request (DSR) function may expose the `default` export, the `enricher` export, or both. Configure a test payload for every implemented export. The Admin Dashboard creation flow currently requires both exports to pass their respective tests.

```typescript
import type { CustomFunction } from '@transcend-io/custom-function-types';

// deno-lint-ignore require-await
export async function enricher({
  payload,
}: CustomFunction.EnricherArgument): Promise<void> {
  console.info(`Enriching ${payload.requestIdentifier.name}`);
}

// deno-lint-ignore require-await
export default async function customFunction({
  payload,
}: CustomFunction.Argument): Promise<void> {
  if (payload.type === 'ACCESS') {
    console.info(`Processing profile ${payload.extras.profile.id}`);
  }
}
```

Install `@transcend-io/custom-function-types` as a dev dependency for local IntelliSense. The `CustomFunction` namespace is already available in the dashboard editor, so no import is needed there. See [Custom Functions Technical Overview](./introduction-to-custom-functions.mdx) for `environment`, `payload`, `sdk`, and `kv`.

### Write the manifest [#write-the-manifest]

Save a `transcend-functions.yml` file in your repository. Each entry maps a function name to a TypeScript source file. Environment values use `<<parameters.ENV_NAME>>` placeholders so secrets stay out of git.

```yaml
# transcend-functions.yml
functions:
  - name: Update Preferences
    code: ./functions/update-preferences.ts
    description: Sync preference changes from an external system into the Preference Store
    test-payload: ./test-payloads/update-preferences.json
    timeout-ms: 30000
    env:
      TRANSCEND_API_KEY: '<<parameters.TRANSCEND_API_KEY>>'
      TRANSCEND_PARTITION: '<<parameters.TRANSCEND_PARTITION>>'

  - name: DSR Lookup
    code: ./functions/dsr-lookup.ts
    type: DSR
    description: Fulfill access and erasure requests against the user warehouse
    test-payloads:
      - payload: ./test-payloads/dsr-lookup.json
        payload-type: DATA_POINT
      - payload: ./test-payloads/dsr-lookup-enricher.json
        payload-type: REQUEST_ENRICHER
    allowed-hosts:
      - warehouse.internal.example.com
    env:
      WAREHOUSE_API_KEY: '<<parameters.WAREHOUSE_API_KEY>>'
```

<Callout type="warn" title="Manifest variable interpolation">
  Parameter interpolation is raw text replacement before YAML parsing. Supplied values must remain valid within the surrounding YAML scalar. Values containing quotes, newlines, or YAML syntax require additional care.
</Callout>

Complete function sources and payload files live in the [custom-functions-manager examples](https://github.com/transcend-io/custom-functions-manager/tree/main/examples).

| Field                       | Required    | Notes                                                                                                                                                  |
| --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`                      | Yes         | Display name. Used as the sync key when no id is set.                                                                                                  |
| `code`                      | Yes         | Path to a self-contained TypeScript source file, relative to and contained within the manifest directory. Bundle local runtime imports into this file. |
| `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`              | Conditional | Omit for a new DSR function and the integration is created for you. Set it to match an existing DSR integration.                                       |
| `sombra-id`                 | No          | Sombra gateway used for creation. An existing function cannot be moved between gateways by changing this value.                                        |
| `sombra-auth-env`           | No          | Name of the environment variable holding that gateway's internal key.                                                                                  |
| `test-payloads`             | No          | JSON files the push must pass. Each item has payload (a file path) and optional payload-type.                                                          |
| `test-payload`              | No          | Shorthand for a single test-payloads item. Mutually exclusive with test-payloads.                                                                      |
| `allowed-hosts`             | No          | Hosts the function may call.                                                                                                                           |
| `timeout-ms`                | No          | Execution timeout in milliseconds.                                                                                                                     |
| `allow-third-party-imports` | No          | Whether the function may import third-party modules.                                                                                                   |
| `env`                       | No          | Object map of environment variables. Use `<<parameters.ENV_NAME>>` placeholders and supply values with `--variables`.                                  |
| `description`               | No          | Human-readable description of the function.                                                                                                            |
| `test-payload-type`         | No          | Export used with test-payload: DATA_POINT or REQUEST_ENRICHER.                                                                                         |

## Push from GitHub Actions [#push-from-github-actions]

On pull requests, install Deno and the CLI, then run the credential-free `custom-functions check` command. On merges to your main branch, use the [transcend-io/custom-functions-manager](https://github.com/transcend-io/custom-functions-manager) composite action to push:

```yaml
name: Sync Transcend Custom Functions

on:
  push:
    branches: [main]
    paths: &custom-function-paths
      - 'transcend-functions.yml'
      - 'functions/**'
      - 'test-payloads/**'
      - 'deno.json*'
      - 'import_map.json'
  pull_request:
    paths: *custom-function-paths

jobs:
  check-custom-functions:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/setup-deno@v2
      - run: npm install --global @transcend-io/cli
      - run: transcend custom-functions check --noInteractive --json --variables=TRANSCEND_API_KEY:placeholder,TRANSCEND_PARTITION:placeholder,WAREHOUSE_API_KEY:placeholder

  push-custom-functions:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: transcend-io/custom-functions-manager@v1
        with:
          api-key: ${{ secrets.TRANSCEND_API_KEY }}
          sombra-auth: ${{ secrets.SOMBRA_INTERNAL_KEY }}
          variables: TRANSCEND_API_KEY:${{ secrets.TRANSCEND_PREFERENCES_API_KEY }},TRANSCEND_PARTITION:${{ secrets.TRANSCEND_PARTITION }},WAREHOUSE_API_KEY:${{ secrets.WAREHOUSE_API_KEY }}
```

`@v1` is the stable major channel, but it is mutable. Pin an exact `cli-version` for tighter control. Fully reproducible workflows also require immutable pins for the Action and its nested actions. Pair `update-manifest: 'true'` with an auto-commit step if you want assigned IDs written back to the repository.

#### Multiple Sombra gateways [#multiple-sombra-gateways]

Set `sombra-id` per manifest entry when functions belong to different gateways. The Action's `sombra-auth` input is the default internal key. When a gateway uses a different key, set `sombra-auth-env` on that entry and export the variable on the Action step.

## How entries match existing functions [#how-entries-match-existing-functions]

1. By `id`, when set. The ID is the sync key. You can rename the function freely. A nonexistent ID fails the push. It never silently creates a duplicate.
2. By exact `name`, when no `id` is set. One match updates it. Zero matches creates it. Multiple functions that share the name fail the push and list the candidate IDs.

Prefer pinning IDs after the first push. Run the CLI with `--updateManifest` (or enable the Action's `update-manifest` input and commit the result). Comments and `<<parameters.x>>` placeholders are preserved. For DSR functions, the assigned `data-silo-id` is written back the same way.

## New DSR functions [#new-dsr-functions]

A new DSR entry that omits `data-silo-id` gets its DSR integration created as part of the push:

1. A Custom Function catalog data silo is created on the entry's Sombra gateway and starts unconfigured.
2. The signed code is test-run against that silo. DSR test payloads get `extras.dataSilo` injected automatically, so payload files never hardcode silo IDs.
3. On a passing test, the function is created and linked. On a failing test, the silo is deleted and the function is rejected.

For the enricher export itself, see [Preflight Check: Custom Function](../dsr-automation/preflight-and-enrichment/preflight-checks/preflight-check-custom-function-enricher.mdx).

## Test before you promote [#test-before-you-promote]

New, changed, or forced entries with configured test payloads are signed against your gateway and tested before the function revision is saved. Unchanged and metadata-only entries are not retested; changes limited to test payloads require `--force`. All configured payloads must pass. If any payload fails, each failing case and its logs are printed, the function revision is not saved, and the command exits 1. External side effects performed by the function cannot be rolled back.

- GENERAL payloads are free-form JSON objects.
- DSR payloads use the webhook notification shape. List one payload per export: `payload-type: DATA_POINT` for the default export, `payload-type: REQUEST_ENRICHER` for the enricher. A warning is printed when a DSR entry covers only one export.
- `--dryRun` never signs or tests. `--skipTests` pushes without running payloads. Entries without test payloads push with a warning.

## Push with the CLI [#push-with-the-cli]

1. Install the CLI in the repository:

   ```shellscript
   npm install --global @transcend-io/cli
   ```

2. Push the manifest:

```shellscript
transcend custom-functions push \
  --auth="$TRANSCEND_API_KEY" \
  --sombraAuth="$SOMBRA_INTERNAL_KEY" \
  --variables="TRANSCEND_API_KEY:$TRANSCEND_PREFERENCES_API_KEY,TRANSCEND_PARTITION:$TRANSCEND_PARTITION,WAREHOUSE_API_KEY:$WAREHOUSE_API_KEY"
```

Useful flags:

- `--dryRun` reports what would change without writing.
- `--promote=false` leaves supported new revisions as drafts for review in the dashboard. It does not apply when creating a new DSR function, which is created active. The default is to promote.
- `--force` pushes a new revision when the CLI sees no diff. Required when you only rotated environment variable values, because those values are encrypted at sign time and cannot be compared.
- `--skipTests` skips the test gate.
- `--updateManifest` writes assigned function IDs (and DSR data-silo IDs) back into the manifest.
- `--variables=ENV_NAME:value,...` fills `<<parameters.ENV_NAME>>` placeholders.
- `--transcendUrl=https://api.us.transcend.io` is required on US hosting. The default is `https://api.transcend.io`.

`transcend custom-functions list` prints the functions in your organization.

## After a push [#after-a-push]

Pushed functions appear in the Functions list like any other. Version history labels those revisions **Pushed via API**.

<Callout type="warn" title="Read-only in the dashboard">
  A function is tied to the channel that last updated it. After you push via the CLI, its code is read-only in the dashboard. Further changes have to come from your codebase.
</Callout>

Two behaviors surprise people:

- Rotating only an environment variable value looks like no change. Push with `--force`.
- Renaming a function without a pinned `id` creates a second function. Run `--updateManifest` once so later renames match correctly.

## Advanced: GraphQL API [#advanced-graphql-api]

Most teams should use the CLI or Action. The following API calls are selected low-level primitives, not a complete replacement for the CLI workflow.

1. Sign the source and execution context against your Sombra customer ingress with `POST /v1/custom/sign`, authenticated with the Transcend API key. Include the Sombra internal key only when the selected gateway requires it. The response contains a `signedCodeJwt` / `signedCodeContextJwt` pair. A complete API workflow must also handle required Sombra or data-system IDs, test execution, version promotion, DSR data-system creation, and rollback.
2. Pass those JWTs to `createCustomFunction` or `updateStandaloneCustomFunction`. The mutations also still accept a dashboard-style `dhEncrypted` payload, which is not the CI path.

The CLI performs these steps, runs test payloads, creates a missing DSR integration, and promotes the new revision. Prefer it unless you are embedding the flow in another toolchain.
