Skip to main content
Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents.
Each function file uses defineLogicFunction() to export a configuration with a handler and optional triggers.
src/logic-functions/createPostCard.logic-function.ts
Available trigger types:
  • httpRoute: Exposes your function on an HTTP path and method. In app code, prefix the route path with /s/ when using RestApiClient; the deployed URL uses the injected TWENTY_FUNCTIONS_URL base (or <server-url>/s when it is not set).
To invoke a route-triggered logic function from a (headless) front component, see Calling a logic function.
  • cron: Runs your function on a schedule using a CRON expression.
  • databaseEvent: Runs on workspace object lifecycle events. When the event operation is updated, specific fields to listen to can be specified in the updatedFields array. If left undefined or empty, any update will trigger the function.
e.g. person.updated, *.created, company.*
  • serverRoute: Exposes a single registration-scoped HTTP route. A resolver function (declared with serverRouteTriggerSettings) runs in the owner workspace and either returns a sync Response or the target workspace AND logic function to enqueue; on the enqueue path the platform acks with 202 and runs that target on the worker queue. See Server route trigger.
You can also manually execute a function using the CLI:
You can watch logs with:

Route trigger payload

When a route trigger invokes your logic function, it receives a RoutePayload object that follows the AWS HTTP API v2 format. Import the RoutePayload type from twenty-sdk/logic-function:
The RoutePayload type has the following structure:

forwardedRequestHeaders

By default, HTTP headers from incoming requests are not passed to your logic function for security reasons. To access specific headers, list them in the forwardedRequestHeaders array:
In your handler, access the forwarded headers like this:
Header names are normalized to lowercase. Access them using lowercase keys (e.g., event.headers['content-type']).

Custom HTTP response

By default, returning a plain value from your handler sends it back as a 200 response (JSON for objects, text/plain for strings). To control the status code and response headers, return a Response from twenty-sdk/logic-function:
For security reasons, response headers are restricted to an allow-list. Any header that is not on the list (e.g. Set-Cookie, CORS headers such as Access-Control-Allow-Origin, or custom X-* headers) is silently dropped before the response is sent. The allowed response headers are:
  • content-type
  • content-language
  • content-disposition
  • cache-control
  • retry-after
The status code must be a valid HTTP status code (between 100 and 599). Response header names are matched case-insensitively.

Server route trigger

httpRouteTriggerSettings exposes a function under /s/ and resolves the workspace from the request host — which works when each workspace has its own domain. Third-party providers, however, deliver every tenant’s events to one URL. For that case, use serverRouteTriggerSettings.The trigger has two parts:
  1. A resolver logic function — declared with serverRouteTriggerSettings — runs in your owner workspace (the workspace that owns the application registration). It inspects the incoming request and returns either:
    • { workspaceId, targetLogicFunctionUniversalIdentifier, payload? } — the platform enqueues that target in the resolved workspace and acks with 202 { queued: true }, or
    • a Response from twenty-sdk/logic-function — the platform echoes that HTTP response synchronously and does not enqueue a target (use this for challenge handshakes such as Slack url_verification).
    The resolver is the single point of authorization — the URL only carries the resolver’s identifier. This is the preferred place to verify request signatures: the resolver runs before any side effect, has access to the original rawBody and forwarded headers, and can reject without ever touching the target.
  2. A target logic function — a regular per-workspace logic function — then runs in the resolved workspace with the payload returned by the resolver (or the original request payload if the resolver didn’t transform it). Its return value is not observed by the HTTP caller when the resolver chose the enqueue path.
src/logic-functions/resolve-server-route.logic-function.ts
src/logic-functions/handle-invoice-paid.logic-function.ts
The endpoint is reachable at:
The identifier is the resolver’s universalIdentifier from your manifest. Register that URL with the provider.
The application must be claimed and installed on its owner workspace. Because the resolver runs in the owner workspace (the workspace that owns the application registration), a server route trigger only works once the application has been claimed — i.e. it has an owner workspace — and that application is installed on the owner workspace. Until both are true the resolver has nowhere to run, so the route cannot be dispatched. An application that exposes a serverRouteTriggerSettings logic function therefore cannot be listed in the marketplace until it is claimed and installed on its owner workspace.
Resolver contract. The SDK’s LogicFunctionConfig type enforces this at compile time: as soon as you set serverRouteTriggerSettings, your handler is constrained to return either a Response, or { workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object } (or a Promise of either). On the dispatch path, the workspaceId must be a workspace where the target function is installed, otherwise the request is rejected with 404. A result that matches neither shape — including one whose identifiers are not UUIDs — is rejected with 502.
Signature verification is your responsibility — verify in the resolver. The platform does not verify request signatures. The resolver is the recommended place to do it: it runs first, with access to event.rawBody and the headers you listed in forwardedRequestHeaders, and a thrown error (or any non-matching workspaceId) stops the dispatch before the target is invoked. If you instead push verification down into the target, the target must be careful not to lose rawBody and headers — i.e. the resolver must not return a payload. Always verify before any side effect and use a constant-time comparison.
For request signatures, most providers sign with HMAC-SHA256; the parts that differ are the header name, the digest encoding, and the signed-payload string. A few examples:The resolver example above already shows the GitHub HMAC-SHA256 flow — adapt the header name, digest encoding, and signed-payload string per the provider you’re integrating.
When the resolver returns a dispatch object, the route responds 202 { queued: true } and the target runs on the worker queue — the caller never observes the target’s latency, result, or failures (those are recorded in execution logs). This keeps sender redeliveries from amplifying processing slowdowns, which is what you want for webhook ingestion.When the caller must read the response body on the same request (challenge handshakes, interactive acknowledgements), return a Response from the resolver instead. The platform echoes it synchronously and skips the queue; its headers go through the same allow-list as HTTP route responses. Keep the resolver fast — some providers (e.g. Slack) time out in a few seconds. Because the resolver is reachable as a public endpoint, protect it with rate limiting at your edge.

Database event trigger payload

When a database event trigger invokes your logic function, it receives one DatabaseEventPayload per changed record. The payload combines metadata about the source workspace and object with the record-level event.
The payload includes:For soft deletes, .deleted follows the update-style shape because the record’s deletedAt field changes. For permanent deletes, use .destroyed.
databaseEventTriggerSettings.updatedFields filters which update events trigger the function. event.properties.updatedFields tells you which fields actually changed on the current event.
Created event example:
Updated event example:
Trigger only on email updates:
Destroyed event example:

Exposing a function as an AI tool or workflow action

Logic functions can be exposed on two surfaces, each with its own trigger:
  • toolTriggerSettings — makes the function discoverable by Twenty’s AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand.
  • workflowActionTriggerSettings — makes the function appear as a step in the visual workflow builder. Uses Twenty’s rich InputSchema so the builder can render proper field editors, variable pickers, and labels.
A function can opt into one, the other, or both. They sit alongside cronTriggerSettings, databaseEventTriggerSettings, and httpRouteTriggerSettings — same pattern, same shape.
Relationship to the workflow Code action. The built-in Code action in the workflow builder is itself a logic function — Twenty creates one per Code step and exposes its editor inline. workflowActionTriggerSettings is how you turn that one-off, inline code into a reusable action: define the function once in your app and it becomes selectable in any workflow, instead of being copy-pasted into each Code step. See the Code action in the user guide for the end-user view.
src/logic-functions/enrich-company.logic-function.ts
Key points:
  • A function can mix surfaces — declare both toolTriggerSettings and workflowActionTriggerSettings to expose it in chat AND in the workflow builder.
  • toolTriggerSettings.inputSchema and workflowActionTriggerSettings.inputSchema are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty’s InputSchema for the workflow action). Provide one explicitly when you want richer typing — for example, with FieldMetadataType-aware fields like CURRENCY or RELATION for the workflow builder, or with description fields the AI agent can read:
To declare your parameters once and serve both surfaces, define a single JSON Schema (InputJsonSchema) and convert it for the workflow action with jsonSchemaToInputSchema from twenty-sdk/logic-function. toolTriggerSettings.inputSchema takes the JSON Schema directly, while workflowActionTriggerSettings.inputSchema expects Twenty’s InputSchema:
A complete workflow action example
workflowActionTriggerSettings accepts four fields:Putting it together — a function exposed as a workflow action, with a declared output so later steps can reference taskId:
src/logic-functions/enrich-company.logic-function.ts
Once the app is installed, Enrich Company appears in the workflow builder’s action picker. The builder renders companyName and domain as input fields (each able to pull values from previous steps), and downstream steps can reference the step’s taskId and enriched outputs.
Write a good description. AI agents rely on the function’s description field to decide when to use the tool. Be specific about what the tool does and when it should be called.
Runtime helpers. twenty-sdk/utils re-exports small runtime helpers so handlers never import from twenty-shared directly. For example, isDefined(value) returns false for both null and undefined — use it to safely narrow optional handler inputs, which can arrive as null at runtime even when typed T | undefined:
Install hooks — pre-install, post-install, and uninstall handlers — share this runtime but are declared with their own define functions and don’t take trigger settings. See Install Hooks for definePreInstallLogicFunction, definePostInstallLogicFunction, and defineUninstallLogicFunction.

Typed API clients (twenty-client-sdk)

The twenty-client-sdk package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components.
CoreApiClient is the main client for querying and mutating workspace data. It is generated from your workspace schema during yarn twenty dev or yarn twenty dev:build, so it is fully typed to match your objects and fields.
The client uses a selection-set syntax: pass true to include a field, use __args for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema.
CoreApiClient is generated at dev/build time. If you use it without running yarn twenty dev or yarn twenty dev:build first, it throws an error. The generation happens automatically — the CLI introspects your workspace’s GraphQL schema and generates a typed client using @genql/cli.

Using CoreSchema for type annotations

CoreSchema provides TypeScript types matching your workspace objects — useful for typing component state or function parameters:
MetadataApiClient ships pre-built with the SDK (no generation required). It queries the /metadata endpoint for workspace configuration, applications, and file uploads.

Uploading files

MetadataApiClient includes an uploadFile method for attaching files to file-type fields:
Key points:
  • Uses the field’s universalIdentifier (not its workspace-specific ID), so your upload code works across any workspace where your app is installed.
  • The returned url is a signed URL you can use to access the uploaded file.
When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables:
  • TWENTY_API_URL — Base URL of the Twenty API
  • TWENTY_APP_ACCESS_TOKEN — Short-lived key scoped to your application’s default function role
You do not need to pass these to the clients — they read from process.env automatically. The API key’s permissions are determined by the role declared with defineApplicationRole() (or referenced via defaultRoleUniversalIdentifier in application-config.ts).