Function SDK reference

Handler exports, input and output shapes, the available SDK calls, hook contracts, error codes, event types, and every limit.

8 min read

The complete contract between your TypeScript and the OmniLab runtime.

Handler exports

Your file must export exactly one handler, named after the function's kind. The build checks that export against the type for that kind, so a missing or wrongly shaped handler fails the build instead of failing in production.

KindExportSignature
Platform eventonPlatformEvent(event: PlatformEvent) => void | Promise<void>
Hook contact.create.preonContactCreatePre(input: ContactCreateInput) => HookOutput<ContactCreateResult>
Hook contact.verificationonContactVerification(input: ContactVerificationInput) => HookOutput<ContactVerificationResult>

The types come from the ./omnilab module, available in every function:

Importing the SDK and its types
import { OmniLab } from "./omnilab";
import type { PlatformEventHandler, ContactCreatePreHandler } from "./omnilab";

Handlers may be async. Only TypeScript is supported, in a single file whose entry point is user.ts.

Input shapes

PlatformEvent

What a platform-event function receives
type PlatformEvent = {
  type: string;   // e.g. "touchpoint.completed.v1"
  id: string;     // unique per event — deduplicate on this
  time: string;   // ISO-8601
  source: string;
  data: Json;     // the event payload, exactly as OmniLab published it
};

Delivery is at-least-once. The same id can arrive more than once, so make external side effects idempotent.

Event payloads

data differs by event type. Every payload carries the same context block, and each family adds its own fields on top.

Present on every event payload
data: {
  web_context:   { utm: { … }, user_agent: { is_bot, is_mobile, is_desktop, is_tablet } },
  interaction:   { interaction_id, interaction_display_name, interaction_public_key, interaction_snapshot_id },
  contact:       { contact_id },
  group_context: { group_id, unique_key },
  tenant_context:{ subdomain, db_name },
}

contact.contact_id is the id you pass to OmniLab.Contact.get and the field writers, so it is the one you will read most.

FamilyAdds to data
touchpoint.* and cta.clicked.v1touchpoint_id, touchpoint_title, touchpoint_type, interactive_totem_type, instant_game_type, event_id
reward.*reward_id, internal_reward_id, reward_display_name, reward_winning_method
reward.won.v1, reward.redeemed.v1the reward fields, plus coupon_code_id and coupon_code_value
contact.created.v1email, firstname, lastname
contact.identified.v1, contact.authenticated.v1email
notification.sent.v1notification_id, channel
feedback.submitted.v1feedback, rating
question.answered.v1question_id, answer

Get the exact payload from Studio rather than from this table

Open your function's Test tab and use Load example. It fills the input editor with a complete, current envelope for any event type in the catalogue — the same shape production delivers. Treat that as the authority and this table as a planning summary, and paste the example straight in to develop against real fields.

Read data defensively even so. Cast it to the shape you expect, and treat every field as possibly absent rather than assuming a payload is fully populated.

Hook contracts

Every hook input carries a context object, but only contact.verification populates its idempotency_key. OmniLab never retries a hook invocation; the key exists because the operation around it can be attempted again, and it stays stable across those attempts — forward it to any provider that accepts one so a repeat cannot cause a duplicate send. An explicit resend deliberately carries a different key.

contact.create.pre receives an empty key. Creating a contact has no external side effect OmniLab owns, so no key is computed for it. If your handler calls out to a provider that charges or mutates something, you have to supply your own dedupe value — the contact's email is the usual choice.

Build your return value with OmniLab.Flow — never return a plain object:

The two possible decisions
OmniLab.Flow.continue(result?)          // proceed, optionally supplying values
OmniLab.Flow.deny(code, message)        // block the operation

contact.create.pre

Runs before a contact is stored, from signup, admin, import, or API.

Input
{
  contact: {
    email: string;
    phone_number: string;
    firstname: string;
    lastname: string;
    custom_fields: Record<string, string>;
  };
  source: string;                    // always "signup" today
  context: { idempotency_key: string };   // empty for this hook point
}

Continuing accepts an optional email, an optional phone_number, and custom_fields. Only the fields you return are applied — omit a field to leave it as submitted. Denying rejects the contact and the operation fails.

The hook runs before the contact is stored and before any connected CRM is updated, so the email you return is the value OmniLab persists and the value it forwards downstream. That is what makes normalizing here worthwhile: returning a lowercased address prevents Foo@example.com and foo@example.com becoming two profiles.

source is declared as a string so more origins can be added, but every call today sends "signup". Don't branch on it yet.

contact.verification

Runs when a double opt-in verification message needs to be sent. Your handler is responsible for actually delivering it.

Input
{
  contact: { email: string; firstname: string; lastname: string };
  interaction: { id: string; public_key: string };
  verification_url: string;
  expires_at: string;
  context: { idempotency_key: string };
}

Continuing accepts an optional provider_message_id, which is recorded in the execution log and changes nothing. Continuing means the dispatch was accepted, not that the message arrived. Denying makes the signup fail.

coupon.assign is not available

A coupon.assign hook point appears in the interface labelled "coming soon", and the SDK still carries its types, but it is not available yet — binding, invoking, and testing it are all refused. Do not build against it until it is released.

SDK surface

These are the only calls that reach outside the sandbox. Everything else in your handler is pure computation.

Config

CallReturnsNotes
OmniLab.Config.get(key)The plugin config value, or undefinedNon-sensitive configuration only
OmniLab.Config.getSecret("bundle.key")The secret valueThrows NOT_FOUND when unset. The bundle must be bound to the function.

Http

CallNotes
OmniLab.Http.fetch(url, options?)The general form
OmniLab.Http.get / post / put / patch / deleteConvenience wrappers

Every request is checked against the function's allowed-hosts list, and internal or private addresses are refused whether or not you listed them. Only http and https are allowed. Each request has a 1500 ms timeout, and a response body larger than 4 MiB is truncated with a header marking it.

Contact

CallReturns
OmniLab.Contact.get(id)Promise<Contact>
OmniLab.Contact.setCustomField(id, key, value)Promise<Contact> — the updated contact
OmniLab.Contact.setCustomFields(id, fields)Promise<Contact> — all or nothing

All three resolve to the same object, so a write hands back the contact it just changed and you rarely need a follow-up read:

Contact
{
  id: string;                              // "contacts/<uuid>"
  firstname: string;
  lastname: string;
  email: string;
  phoneNumber: string;
  externalId: string;
  barcode: string;
  customFields: Record<string, string>;    // the only writable part
  emailVerified: boolean;
  optin: boolean;
  hasAcceptedTerms: boolean;
  isBlacklisted: boolean;
  blacklistReason: string;
  originInteraction: string;
}

Every field except customFields is read-only from a function. Custom field values are always strings.

Custom field keys beginning system. or feature. are reserved and rejected with INVALID_ARGUMENT. A key can be up to 128 characters and a value up to 4 KB. Prefer setCustomFields for several keys: it is one call against your budget rather than several.

Logging

console.log, console.info, console.warn, console.error, console.debug, and console.trace are captured into the execution record and shown in the Test tab. Capture is limited to 16 KB total, 256 lines, and 2 KB per line; beyond that the log is marked truncated.

Designing within the SDK

The calls above are the whole surface. When your logic needs more, reach your own systems over HTTP and do the work there. Three consequences to plan around:

Keep your state outside the function. Every invocation starts fresh, so a value you assign at module level is gone by the next one. Persist what you need to contact custom fields, or to your own service.

Reach other OmniLab records through the API. Functions read and write contacts. For transactions, rewards, smart links, and everything else, call the OmniLab API from your own systems instead.

Functions run on triggers, not on a clock. There is no way to schedule one. If you need work to happen at a set time, run it from your own scheduler.

Error codes

SDK failures throw a typed error carrying a code, a message, and a retryable flag.

CodeRetryableTypical cause
NOT_FOUNDNoAn unset secret, or a contact that does not exist
PERMISSION_DENIEDNoAn operation the function is not allowed to perform
INVALID_ARGUMENTNoA reserved custom field key, or a malformed value
ALREADY_EXISTSNoA conflicting write
EGRESS_BLOCKEDNoThe hostname is not allow-listed, or resolves to a private address
TIMEOUTYesAn HTTP request exceeded its 1500 ms limit
UNAVAILABLEYesA dependency was temporarily unreachable
INTERNALYesAn unexpected platform error

Error messages are meant for a human reading the execution log, and their wording is not stable. Never parse one — branch on the code.

Failure semantics

TriggerOn failureRetries
HookFails closed — the operation is denied. Applies to handler errors, timeouts, and output that breaks the contract.None
Platform eventThe activity behind the event is unaffected; the failed run is recordedA transient problem is retried with increasing backoff, up to five attempts, and then stops. A permanent problem — the function was deleted, or the event could not be read — stops immediately. A handler that throws is not retried, because the same code would throw again.

Subscribable event types

These are the event types the picker offers, grouped as it groups them and searchable within it. If OmniLab emits a type that is not listed, you can add it with the custom event field.

FamilyShown asEvent type
TouchpointStartedtouchpoint.started.v1
TouchpointParticipatedtouchpoint.participated.v1
TouchpointParticipation form filledtouchpoint.participation_form_filled.v1
TouchpointCompletedtouchpoint.completed.v1
TouchpointTerms acceptedtouchpoint.terms_accepted.v1
TouchpointCTA clickedcta.clicked.v1
RewardEligiblereward.eligible.v1
RewardWonreward.won.v1
RewardLostreward.lost.v1
RewardRedeemedreward.redeemed.v1
RewardTemporary wonreward.temporary_won.v1
RewardTemporary lostreward.temporary_lost.v1
RewardTemporary blockedreward.temporary_blocked.v1
RewardExpiredreward.expired.v1
ContactCreatedcontact.created.v1
ContactIdentifiedcontact.identified.v1
ContactAuthenticatedcontact.authenticated.v1
OtherNotification sentnotification.sent.v1
OtherFeedback submittedfeedback.submitted.v1
OtherQuestion answeredquestion.answered.v1

Two platform rules apply on top of this list. Anonymous page-visit events are never subscribable, and OmniLab refuses a subscription that names one. And an event that carries no identified contact never triggers a function, whatever your subscription says.

Filters accept a closed set of keys — tenant, group, interaction, and contact — combined with AND and matched exactly.

Limits

Per invocation

LimitValue
Timeout50 – 2000 ms, default 2000
Memory1 – 64 pages at 64 KiB each, default 32 pages (2 MiB)
SDK calls20
HTTP request timeout1500 ms
HTTP response body4 MiB, truncated beyond

The timeout covers your handler only. The time OmniLab spends preparing to run it is not charged against your limit — the execution detail reports the two separately.

Per function

LimitValue
Source size1 MB
Allowed hosts16
Plugin config32 keys, 4 KB total
Secret bundles bound5
Active subscriptions per event type5 per tenant
Active hook bindings per hook point1 global, plus 1 per group
Builds in flight1

Per tenant

LimitValue
Secret bundles5
Secrets per bundle32
Secret bundle keyUp to 32 characters, lowercase letters, digits, and underscores, starting with a letter

Retention and truncation

ItemValue
Execution records24 hours
Recorded input and output256 KB per side, truncated beyond, with original sizes reported
Captured logs16 KB total, 256 lines, 2 KB per line
Build log shown in StudioThe last 8 KB

Build

LimitValue
Type check when you save10 seconds
Build4 minutes
A stalled build is released, so you can start another15 minutes

There are no rate limits on invocation. Volume is bounded instead by the SDK call budget, the subscription cap, and the exclusion of anonymous page-visit events.

On this page