Saltar para o conteúdo principal
Funções de lógica são executadas de forma isolada em processos Node.js de curta duração — assim que uma execução termina, nada que foi mantido na memória sobrevive. Quando você precisa lembrar de algo entre execuções (armazenar em cache uma resposta cara de uma API, guardar um cursor para sincronizações incrementais, aplicar debounce ao trabalho ou passar estado de uma função para outra), persista isso no banco de dados do workspace. Você não precisa de uma primitiva de armazenamento dedicada para isso: um pequeno objeto técnico com um campo key e um campo value oferece um armazenamento durável de chave-valor, com escopo para o workspace, consultável por meio do mesmo cliente de API tipado que você já usa para registros.
  ┌─────────────────┐   set(key, value)    ┌──────────────────────────┐
  │ Logic function  │ ───────────────────▶ │ "KV Store" object        │
  │ (your handler)  │ ◀─────────────────── │  key (unique)  │  value  │
  └─────────────────┘   get(key)           └──────────────────────────┘

Definir o objeto de armazenamento

Declare um objeto personalizado com dois campos — key (um TEXT único) e value (um RAW_JSON, para que você possa armazenar qualquer payload serializável em JSON). Consulte Objects para a referência completa de defineObject.
src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';

export const KV_STORE_UNIVERSAL_IDENTIFIER =
  '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33';
export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER =
  '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21';
export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER =
  '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58';

export default defineObject({
  universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER,
  nameSingular: 'kvStore',
  namePlural: 'kvStores',
  labelSingular: 'KV Store',
  labelPlural: 'KV Store',
  description: 'Key-value storage for logic functions',
  icon: 'IconDatabase',
  fields: [
    {
      universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
      name: 'key',
      type: FieldType.TEXT,
      label: 'Key',
      description: 'Unique lookup key',
      icon: 'IconKey',
    },
    {
      universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER,
      name: 'value',
      type: FieldType.RAW_JSON,
      label: 'Value',
      description: 'Stored JSON payload',
      icon: 'IconJson',
    },
  ],
});

Aplicar unicidade à chave

Adicione um índice exclusivo em key para que a mesma chave nunca possa ter duas linhas. Esta é a primitiva recomendada para unicidade — consulte Data → Unique indexes.
src/indexes/kv-store-key.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
  KV_STORE_UNIVERSAL_IDENTIFIER,
  KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/kv-store.object';

export default defineIndex({
  universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14',
  objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER,
  isUnique: true,
  fields: [
    {
      universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15',
      fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
    },
  ],
});

Ler e gravar a partir de uma função de lógica

Encapsule o objeto por trás de alguns pequenos helpers para que o restante do seu código pareça uma API de chave-valor — get, set e del. Eles usam CoreApiClient, que é gerado a partir do schema do seu workspace e totalmente tipado em relação ao objeto kvStore.
src/logic-functions/handlers/kv-store.ts
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-sdk/utils';

const client = new CoreApiClient();

// Look up a single row by its key.
const findByKey = async (key: string) => {
  const { kvStores } = await client.query({
    kvStores: {
      __args: { filter: { key: { eq: key } }, first: 1 },
      edges: { node: { id: true, value: true } },
    },
  });

  return kvStores.edges[0]?.node;
};

// Read a value. Returns undefined when the key is missing.
export const get = async <TValue>(key: string): Promise<TValue | undefined> => {
  const row = await findByKey(key);

  return isDefined(row) ? (row.value as TValue) : undefined;
};

// Write a value. Creates the row on first write, updates it afterwards (upsert).
export const set = async (key: string, value: unknown): Promise<void> => {
  const existing = await findByKey(key);

  if (isDefined(existing)) {
    await client.mutation({
      updateKvStore: {
        __args: { id: existing.id, data: { value } },
        id: true,
      },
    });
    return;
  }

  await client.mutation({
    createKvStore: {
      __args: { data: { key, value } },
      id: true,
    },
  });
};

// Delete a value. No-op when the key is missing.
export const del = async (key: string): Promise<void> => {
  const existing = await findByKey(key);

  if (isDefined(existing)) {
    await client.mutation({
      deleteKvStore: { __args: { id: existing.id }, id: true },
    });
  }
};
O índice exclusivo protege contra duplicatas, mas duas execuções gravando a mesma nova chave no mesmo instante ainda podem competir entre a busca e a criação. Trate uma criação que falhar na restrição de unicidade como “alguém mais venceu” — capture o erro e leia novamente, ou tente novamente como uma atualização.

Use-o: armazenar em cache uma chamada cara

Um uso típico é armazenar em cache uma resposta lenta ou com limite de taxa de terceiros, para que execuções repetidas a reutilizem em vez de pagar o custo todas as vezes.
src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';

const ONE_HOUR_MS = 60 * 60 * 1000;

type CachedRate = { rate: number; fetchedAt: number };

const handler = async (params: { from: string; to: string }) => {
  const cacheKey = `exchange-rate:${params.from}:${params.to}`;
  const cached = await get<CachedRate>(cacheKey);

  if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
    return { rate: cached.rate, cached: true };
  }

  const response = await fetch(
    `https://api.example.com/rate?from=${params.from}&to=${params.to}`,
  );
  const { rate } = (await response.json()) as { rate: number };

  await set(cacheKey, { rate, fetchedAt: Date.now() });

  return { rate, cached: false };
};

export default defineLogicFunction({
  universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
  name: 'get-exchange-rate',
  timeoutSeconds: 10,
  handler,
});

Padrões e dicas

  • Namespacing. Prefixe chaves para manter diferentes responsabilidades separadas e facilitar buscas em lote — sync-cursor:linear, cache:exchange-rate:USD:EUR, lock:nightly-report. Filtre com key: { like: 'cache:%' } para listar ou limpar todo um namespace.
  • Expiração (TTL). O armazenamento não possui expiração integrada. Armazene um carimbo de data/hora dentro de value (como no exemplo de cache) e verifique-o na leitura, ou adicione um campo DATE_TIME e limpe periodicamente linhas obsoletas a partir de uma função acionada por cron.
  • O que armazenar. RAW_JSON comporta qualquer valor serializável em JSON — números, strings, arrays, objetos. Mantenha as entradas pequenas; isto é para coordenação e cache, não para blobs grandes ou arquivos. Para arquivos, use um campo FILES e uploadFile.
  • Visibilidade e permissões. As linhas vivem no banco de dados do workspace como qualquer outro registro, portanto podem ser consultadas pela API e respeitam a role do seu app. Para manter o armazenamento fora da interface principal, deixe-o de fora do seu menu de navegação.
  • Escopo para um registro. Precisa de estado por registro em vez de chaves globais? Adicione uma relation do objeto de armazenamento para o objeto de destino em vez de codificar o id na chave.
Isto é uma convenção, não um recurso separado — o “KV Store” é apenas um objeto personalizado comum que você define e consulta com a API padrão. Isso significa que ele se beneficia da mesma sincronização, permissões e ferramentas que o restante dos dados do seu app.