> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Logic Functions

> Define server-side TypeScript functions with HTTP, cron, and database event triggers.

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.

    ```ts src/logic-functions/createPostCard.logic-function.ts theme={null}
    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:

    * **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:

    > e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create`

    * **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.*`

    <Note>
      You can also manually execute a function using the CLI:

      ```bash filename="Terminal" theme={null}
      yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
      ```

      ```bash filename="Terminal" theme={null}
      yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
      ```

      You can watch logs with:

      ```bash filename="Terminal" theme={null}
      yarn twenty logs
      ```
    </Note>

    #### Route trigger payload

    When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the
    [AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
    Import the `RoutePayload` type from `twenty-sdk`:

    ```ts theme={null}
    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                                                                                                                                                                                      |                                                                            |

    #### 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:

    ```ts theme={null}
    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:

    ```ts theme={null}
    const handler = async (event: RoutePayload) => {
      const signature = event.headers['x-webhook-signature'];
      const contentType = event.headers['content-type'];

      // Validate webhook signature...
      return { received: true };
    };
    ```

    <Note>
      Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`).
    </Note>

    #### 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.

    ```ts src/logic-functions/enrich-company.logic-function.ts theme={null}
    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:

    * 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:

    ```ts theme={null}
    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'],
        },
      },
    });
    ```

    <Note>
      **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.
    </Note>
  </Accordion>
</AccordionGroup>

<Note>
  **Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`.
</Note>

## 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.

| 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    |

<AccordionGroup>
  <Accordion title="CoreApiClient" description="Query and mutate workspace data (records, objects)">
    `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.

    ```ts theme={null}
    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.

    <Note>
      **CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty 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`.
    </Note>

    #### Using CoreSchema for type annotations

    `CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters:

    ```ts theme={null}
    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);
    ```
  </Accordion>

  <Accordion title="MetadataApiClient" description="Workspace config, applications, and file uploads">
    `MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads.

    ```ts theme={null}
    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 },
        },
      },
    });
    ```

    #### Uploading files

    `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields:

    ```ts theme={null}
    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:

    * 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.
  </Accordion>
</AccordionGroup>

<Note>
  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`).
</Note>
