Channel Developers

thumbnail

Extension Guide

This document is maintained with the Channel App SDK. Check the SDK README first for current packages and the complete reading order. The source of truth for this Document is the GitHub source.

An Extension is a named, versioned contract that connects typed app Functions to a standard Channel capability. Channel surfaces know how to discover and invoke a command, widget, custom tab, hook, OAuth flow, or other capability because the app implements that Extension's official Function names and schemas.

An Extension normally contains two kinds of Functions:

Function kind

Purpose

Example

Metadata or discovery

Describes the capability and points to runtime Functions

extension.command.metadata.getCommands

Runtime or action

Performs the user-visible or background operation

extension.command.command.execute

Metadata may reference a standalone app Function such as orders.sync. Keep app-specific business operations standalone, and use the Extension namespace only for the standard contract. Inside an Extension, a relative name becomes extension.{extensionName}.{relativeName}.

Registration does not upload or deploy app code. It announces the app-level (extensionName, systemVersion) contract so AppStore can call the configured Function Endpoint and discover schemas. It also does not install or enable the app in an individual Channel; installation, permission grants, and capability activation are separate steps.

Use the SDK-owned Extension family, Function names, and schemas. A metadata response that points to an action Function does not create that Function automatically; both the metadata Function and every referenced runtime Function must be registered in the app server.

Use @Extension for the family and system version, @Func for relative names, and register the decorated class as a NestJS provider. ChannelAppModule discovers the provider and performs the recommended registration flow after the HTTP listener is ready.

If the class is missing from providers, discovery cannot see it. If autoRegister is false, the SDK still dispatches implemented Functions, but it does not publish their Extension target to AppStore.

Use the typed extension/{family} builder with app.Use. The builder declares both the Function schemas and the Extension target. server.WithAutoRegister() starts registration after the server can answer discovery requests.

Go

Use server.WithAutoRegisterRetry and server.WithAutoRegisterResult when deployment policy needs custom retry or observability. Existing Gin servers use the equivalent options from server/gin.

The recommended auto-registration flow:

  1. waits until the Function server is listening;

  2. gets one cached app token through TokenManager;

  3. calls registerExtension for every discovered Extension name and system version;

  4. retries transient failures with bounded exponential backoff;

  5. lets AppStore call the versioned Function Endpoint for schema and metadata discovery.

The request fields are camelCase: appId, extensionName, and systemVersion. systemVersion such as v1 is the Channel Extension contract version, not the release version of your app.

Only custom bootstraps or deployment-controlled registration normally call the native Function directly:

SDK

Explicit call

TypeScript

nativeClient.registerExtension(appId, extensionName, systemVersion, appToken.accessToken)

Go

nativeClient.RegisterExtension(ctx, appToken.AccessToken, appID, extensionName, systemVersion)

Do not issue a new token or register on every Function request. Apps with only standalone Functions use the SDK's core:v1 fallback. Advanced families such as ALF task, Notebook, and Messaging may require a secondary sync or coordinated product setup after generic Extension registration; follow their family recipe.

  • Deploy the Function Endpoint before registration because AppStore may call discovery immediately.

  • Keep auto-registration enabled on normal startup and use its bounded retry instead of a custom infinite loop.

  • In a multi-replica deployment, use shared token storage. Duplicate idempotent registration calls are acceptable, but every replica must not issue its own uncached token loop.

  • Re-register after changing Extension schemas, metadata, Function names, permissions, or the Function Endpoint. Do not change systemVersion merely for an app release.

  • Use unregisterExtension only when intentionally removing a capability; deployment rollback alone should restore the last compatible server and schemas.

Verify these boundaries separately:

  1. startup logs show the expected Extension name, system version, and successful registration;

  2. getFunctions discovery contains every metadata and referenced runtime Function;

  3. metadata appears on the intended Channel surface after installation and activation;

  4. one real runtime call succeeds in a test Channel;

  5. invalid input, invalid signature, missing permission, and transient registration failure are rejected or retried as designed.

A successful registerExtension response proves only that the registration request was accepted. It does not prove that discovery, metadata validation, Channel installation, activation, or runtime handlers work.

For every Extension:

  1. enable only the permissions used by its Functions;

  2. implement metadata and referenced Functions with SDK schemas;

  3. use SDK auto-registration with an app token;

  4. test discovery, valid calls, invalid input, missing authorization, and retries;

  5. keep App Secret, Signing Key, app/channel tokens, and provider credentials out of WAM code.

TypeScript apps normally use @Extension and @Func. Go apps should prefer the typed extension/{family} builder. Each family recipe below covers both languages, authentication, WAM, reliability, and testing, then links the exact TypeScript schemas and Go Extension reference.

Read Function registration for the shared wire contract and apply the WAM guide to any Extension that opens UI.

Use config for API keys, client_credentials, shop identifiers, and other scoped settings. Implement extension.config.metadata.getConfigSchema. Optional validation, save, and delete Functions may enforce provider rules. Mark secrets as credentials, localize labels rather than stable keys, and read injected values from Function context instead of sending them to a WAM.

Config recipe

Use oauth only for a provider's Authorization Code flow. Implement extension.oauth.metadata.getAuthConfig and register oauth:v1. AppStore owns redirect state and injects the connected provider token as ctx.authToken. Do not use this Extension for API keys or client_credentials; those belong in Config.

OAuth recipe

extension.command.metadata.getCommands publishes Desk commands. Each command must reference the exact full name of a standalone or Extension Function. Use a command to return text, perform an action, or open a WAM. Test command discovery separately from the referenced action handler.

Command recipe · WAM guide · TypeScript tutorial · Go tutorial

extension.widget.metadata.getWidgets publishes contextual widgets. Widget metadata selects the surface and action Function; the action can return a WAM. Treat chat, user, and manager fields as surface-dependent optional context and verify permissions for every native action.

Widget recipe

extension.customtab.metadata.getCustomTabs publishes app-owned tabs. Keep tab identifiers stable, point actions to exact Function names, and use a WAM for interactive content. Do not place tokens or private records in tab metadata or wamArgs.

Custom tab recipe

extension.hook.metadata.getHooks declares event-driven Functions. Make handlers idempotent, authenticate signed app Function calls, and return quickly when the event can be processed asynchronously. Public webhook.received targets require a public targetId, a high-entropy endpointToken, payload validation, replay protection, and secret rotation.

Hook recipe

extension.polling.metadata.getPollers declares scheduled pollers. A target resolver such as target.getChannels pages through installed channels, and each poller names a full Function to invoke. Store cursors durably, make retries idempotent, bound each batch, and test partial failure.

Polling recipe

Use calendar for calendars, event types, availability, booking creation, cancellation, and queries. Keep provider credentials server-side, normalize time zones explicitly, and make booking mutations idempotent. A WAM is appropriate for slot selection while server Functions own provider calls.

Calendar recipe

extension.store.metadata.getStoreProfile publishes store identity and presentation metadata. AppStore reads the profile during registration or synchronization. Keep stable IDs separate from localized labels and do not include provider credentials in the profile.

Store recipe

DataSource metadata exposes catalogs, tables, columns, and table descriptions. Query execution uses the authenticated DataSource gRPC endpoint rather than the normal app Function route. Validate x-access-token, enforce catalog/table allowlists, parameterize SQL, cap rows and time, and stream Arrow-compatible results. The SDK includes PostgreSQL and BigQuery-oriented runners.

DataSource recipe · Go examples

Use the redesigned commerce Extension for new commerce apps. It provides the ID-based order model, buyer information, order lookup, cancel/return/exchange requests, exchangeable items, shipping address changes, and structured ActionResult responses. Validate provider state before mutations and return explicit unsupported results when a provider lacks an operation.

Commerce details

wms connects warehouse/order-management providers. Use the ID-based extension.wms.order.* Functions for order lookup, cancel/return/exchange restore flows, and shipping-address changes. Require explicit shop configuration and test reversible mutations in a safe environment.

WMS details

Messaging covers inbox, prebuilt messaging, follow-up, medium-link, and CHX integrations. It is more AppStore-driven than other families and still uses generic registration plus several channel-scoped native Functions. Design the required native claims first, persist external conversation/message mappings, make webhook or polling delivery idempotent, and never impersonate a user without the proper user/manager authorization.

Messaging recipe

extension.alfTask.alftask.getTasks publishes versioned automation tasks. Registration has two steps: registerExtension("alfTask", "v1") and registerAlfTasks. Keep task keys stable, increment versions for behavior changes, and verify the synchronized versions.

ALF task recipe

extension.notebook.core.getNotebooks publishes versioned notebook definitions. Registration also requires registerAppNotebooks. Keep notebook and cell keys stable, increment versions for definition changes, and treat rendered content as untrusted when it includes external data.

Notebook recipe

mailRelay receives normalized mail events through extension.mailRelay.inbound.onMailReceived. TypeScript 0.17.2 registers that full Function name as a standalone @Func and calls registerExtension("mailRelay", "v1") explicitly; Go provides a typed builder. Validate relay tokens, bound attachments and body size, deduplicate message IDs, and avoid logging raw mail content.

Mail relay recipe

  • Metadata uses the exact SDK schema and full Function names.

  • The Extension class/provider or Go builder is registered once.

  • Function requests reject missing or invalid signatures.

  • App/channel tokens are cached and refreshed; manager/user authorization remains in the WAM host.

  • Provider credentials are injected from Config/OAuth and never returned to the client.

  • Mutations are idempotent or safely retryable and have explicit permission-failure behavior.

  • Discovery and at least one real invocation pass in an installed test app.

After implementation passes, use the production readiness guide as the final security, reliability, deployment, operations, and rollback gate.