Salt la conținutul principal
Funcțiile logice rulează în sandbox în procese Node.js de scurtă durată — odată ce o rulare se termină, nimic din ce a fost păstrat în memorie nu supraviețuiește. Când ai nevoie să îți amintești ceva între rulări (să păstrezi în cache un răspuns API costisitor, să stochezi un cursor pentru sincronizări incrementale, să aplici debounce sau să transmiți starea de la o funcție la alta), salvează-l în baza de date a workspace-ului. Nu ai nevoie de o primitivă de stocare dedicată pentru asta: un mic obiect tehnic cu un câmp key și un câmp value îți oferă o stocare cheie-valoare durabilă, limitată la workspace, care poate fi interogată prin același client API tipizat pe care îl folosești deja pentru înregistrări.
  ┌─────────────────┐   set(key, value)    ┌──────────────────────────┐
  │ Logic function  │ ───────────────────▶ │ "KV Store" object        │
  │ (your handler)  │ ◀─────────────────── │  key (unique)  │  value  │
  └─────────────────┘   get(key)           └──────────────────────────┘

Definește obiectul de stocare

Declară un obiect personalizat cu două câmpuri — key (un TEXT unic) și value (un RAW_JSON astfel încât să poți stoca orice payload serializabil JSON). Vezi Objects pentru referința completă 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',
    },
  ],
});

Aplică unicitatea cheii

Adaugă un index unic pe key astfel încât aceeași cheie să nu poată avea niciodată două rânduri. Acesta este primitiva recomandată pentru unicitate — vezi 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,
    },
  ],
});

Citește și scrie dintr-o funcție logică

Înfășoară obiectul în câțiva helperi mici astfel încât restul codului tău să se comporte ca un API de tip cheie-valoare — get, set și del. Aceștia folosesc CoreApiClient, care este generat din schema workspace-ului tău și este complet tipizat față de obiectul 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 },
    });
  }
};
Indexul unic protejează împotriva duplicatelor, dar două rulări care scriu aceeași cheie nouă în același moment pot totuși să intre în cursă între căutare și creare. Tratează o creare care eșuează pe constrângerea de unicitate ca pe „altcineva a câștigat” — intercepteaz-o și recitește, sau reîncearcă sub formă de actualizare.

Folosește-l: păstrează în cache un apel costisitor

O utilizare tipică este păstrarea în cache a unui răspuns lent sau cu limitare de rată de la un terț, astfel încât rulările repetate să îl refolosească în loc să plătească costul de fiecare dată.
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,
});

Tipare și sfaturi

  • Spații de nume. Prefixează cheile pentru a separa domeniile diferite și pentru a face ușoare căutările în masă — sync-cursor:linear, cache:exchange-rate:USD:EUR, lock:nightly-report. Filtrează cu key: { like: 'cache:%' } pentru a lista sau a curăța un întreg namespace.
  • Expiry (TTL). Store-ul nu are expirare integrată. Stochează un timestamp în interiorul câmpului value (ca în exemplul de cache) și verifică-l la citire, sau adaugă un câmp DATE_TIME și curăță periodic rândurile vechi dintr-o funcție declanșată de cron.
  • Ce să stochezi. RAW_JSON poate conține orice valoare serializabilă JSON — numere, stringuri, array-uri, obiecte. Păstrează intrările mici; acesta este pentru coordonare și caching, nu pentru blob-uri mari sau fișiere. Pentru fișiere, folosește un câmp FILES și uploadFile.
  • Vizibilitate și permisiuni. Rândurile trăiesc în baza de date a workspace-ului ca orice altă înregistrare, astfel încât pot fi interogate prin API și respectă rolul aplicației tale. Pentru a ține store-ul în afara UI-ului principal, nu îl include în navigation menu.
  • Limitare la o înregistrare. Ai nevoie de stare per înregistrare în loc de chei globale? Adaugă o relație de la obiectul de stocare la obiectul țintă, în loc să codifici ID-ul în cheie.
Aceasta este o convenție, nu o funcționalitate separată — „KV Store-ul” este doar un obiect personalizat obișnuit pe care îl definești și îl interoghezi cu API-ul standard. Asta înseamnă că beneficiază de același mecanism de sincronizare, aceleași permisiuni și același tooling ca restul datelor aplicației tale.