packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx
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.
<AccordionGroup> <Accordion title="defineLogicFunction" description="Define logic functions and their triggers">Each function file uses defineLogicFunction() to export a configuration with a handler and optional triggers.
import { defineLogicFunction } from 'twenty-sdk/define';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'POST',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
Available trigger types:
/s/ endpoint:e.g.
path: '/post-card/create'is callable athttps://your-twenty-server.com/s/post-card/create
updated, specific fields to listen to can be specified in the updatedFields array. If left undefined or empty, any update will trigger the function.<Note> You can also manually execute a function using the CLI:e.g.
person.updated,*.created,company.*
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
You can watch logs with:
yarn twenty logs
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:
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
The RoutePayload type has the following structure:
| Property | Type | Description | Example |
|---|---|---|---|
headers | Record<string, string | undefined> | HTTP headers (only those listed in forwardedRequestHeaders) | see section below |
queryStringParameters | Record<string, string | undefined> | Query string parameters (multiple values joined with commas) | /users?ids=1&ids=2&ids=3&name=Alice -> { ids: '1,2,3', name: 'Alice' } |
pathParameters | Record<string, string | undefined> | Path parameters extracted from the route pattern | /users/:id, /users/123 -> { id: '123' } |
body | object | null | Parsed request body (JSON) | { id: 1 } -> { id: 1 } |
rawBody | string | undefined | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's X-Hub-Signature-256, Stripe). undefined when the runtime did not preserve it. | |
isBase64Encoded | boolean | Whether the body is base64 encoded | |
requestContext.http.method | string | HTTP method (GET, POST, PUT, PATCH, DELETE) | |
requestContext.http.path | string | Raw request path |
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:
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
In your handler, access the forwarded headers like this:
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
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.
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
Key points:
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:export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
The twenty-client-sdk package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components.
| Client | Import | Endpoint | Generated? |
|---|---|---|---|
CoreApiClient | twenty-client-sdk/core | /graphql — workspace data (records, objects) | Yes, at dev/build time |
MetadataApiClient | twenty-client-sdk/metadata | /metadata — workspace config, file uploads | No, ships pre-built |
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 build, so it is fully typed to match your objects and fields.
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
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.
CoreSchema provides TypeScript types matching your workspace objects — useful for typing component state or function parameters:
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
MetadataApiClient ships pre-built with the SDK (no generation required). It queries the /metadata endpoint for workspace configuration, applications, and file uploads.
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
MetadataApiClient includes an uploadFile method for attaching files to file-type fields:
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
| Parameter | Type | Description |
|---|---|---|
fileBuffer | Buffer | The raw file contents |
filename | string | The name of the file (used for storage and display) |
contentType | string | MIME type (defaults to application/octet-stream if omitted) |
fieldMetadataUniversalIdentifier | string | The universalIdentifier of the file-type field on your object |
Key points:
universalIdentifier (not its workspace-specific ID), so your upload code works across any workspace where your app is installed.url is a signed URL you can use to access the uploaded file.TWENTY_API_URL — Base URL of the Twenty APITWENTY_APP_ACCESS_TOKEN — Short-lived key scoped to your application's default function roleYou 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).
</Note>