Zum Hauptinhalt springen
Logikfunktionen laufen isoliert in kurzlebigen Node.js-Prozessen – sobald ein Durchlauf abgeschlossen ist, überlebt nichts, was im Speicher gehalten wurde. Wenn Sie sich zwischen Durchläufen etwas merken müssen (eine teure API-Antwort zwischenspeichern, einen Cursor für inkrementelle Synchronisierungen speichern, Arbeit entprellen oder Zustand von einer Funktion an eine andere übergeben), speichern Sie es in der Workspace-Datenbank. Dafür benötigen Sie kein spezielles Speicher-Primitiv: Ein kleines technisches Objekt mit einem key-Feld und einem value-Feld gibt Ihnen einen dauerhaften Key-Value-Store, der auf den Workspace begrenzt ist und über denselben typisierten API-Client abfragbar ist, den Sie bereits für Datensätze verwenden.
  ┌─────────────────┐   set(key, value)    ┌──────────────────────────┐
  │ Logic function  │ ───────────────────▶ │ "KV Store" object        │
  │ (your handler)  │ ◀─────────────────── │  key (unique)  │  value  │
  └─────────────────┘   get(key)           └──────────────────────────┘

Das Store-Objekt definieren

Deklarieren Sie ein benutzerdefiniertes Objekt mit zwei Feldern – key (ein eindeutiger TEXT) und value (ein RAW_JSON, damit Sie jede JSON-serialisierbare Nutzlast speichern können). Siehe Objekte für die vollständige defineObject-Referenz.
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',
    },
  ],
});

Eindeutigkeit des Schlüssels erzwingen

Fügen Sie einen eindeutigen Index auf key hinzu, damit derselbe Schlüssel niemals zwei Zeilen haben kann. Dies ist das empfohlene Primitiv für Eindeutigkeit – siehe Daten → Eindeutige Indizes.
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,
    },
  ],
});

Aus einer Logikfunktion lesen und schreiben

Kapseln Sie das Objekt hinter ein paar kleinen Hilfsfunktionen, sodass der Rest Ihres Codes wie eine Key-Value-API aussieht – get, set und del. Sie verwenden CoreApiClient, der aus Ihrem Workspace-Schema generiert wird und vollständig gegen das kvStore-Objekt typisiert ist.
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 },
    });
  }
};
Der eindeutige Index schützt vor Duplikaten, aber zwei Durchläufe, die denselben neuen Schlüssel im gleichen Moment schreiben, können zwischen der Abfrage und dem Erstellen dennoch in eine Race-Condition geraten. Behandeln Sie einen Erstellvorgang, der an der Eindeutigkeitsbeschränkung scheitert, als „jemand anderes war schneller“ – fangen Sie den Fehler ab und lesen Sie erneut, oder versuchen Sie es als Aktualisierung noch einmal.

Verwenden Sie ihn: einen teuren Aufruf zwischenspeichern

Eine typische Verwendung ist das Zwischenspeichern einer langsamen oder ratelimitierten Antwort eines Drittanbieters, sodass wiederholte Durchläufe sie wiederverwenden, anstatt jedes Mal die Kosten zu tragen.
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,
});

Muster & Tipps

  • Namespacing. Präfixieren Sie Schlüssel, um unterschiedliche Belange getrennt zu halten und Bulk-Abfragen zu erleichtern – sync-cursor:linear, cache:exchange-rate:USD:EUR, lock:nightly-report. Filtern Sie mit key: { like: 'cache:%' }, um einen gesamten Namespace aufzulisten oder zu leeren.
  • Ablauf (TTL). Der Store hat keine eingebaute Ablaufzeit. Speichern Sie einen Zeitstempel innerhalb des value (wie im Cache-Beispiel) und prüfen Sie ihn beim Lesen, oder fügen Sie ein DATE_TIME-Feld hinzu und löschen Sie regelmäßig veraltete Zeilen aus einer cron-getriggerten Funktion.
  • Was gespeichert wird. RAW_JSON enthält jeden JSON-serialisierbaren Wert – Zahlen, Zeichenketten, Arrays, Objekte. Halten Sie Einträge klein; dies ist für Koordination und Caching gedacht, nicht für große Blobs oder Dateien. Für Dateien verwenden Sie ein FILES-Feld und uploadFile.
  • Sichtbarkeit & Berechtigungen. Zeilen befinden sich wie jeder andere Datensatz in der Workspace-Datenbank, sind also über die API abfragbar und respektieren die Rolle Ihrer App. Um den Store aus dem Haupt-UI herauszuhalten, führen Sie ihn nicht in Ihrem Navigationsmenü auf.
  • Auf einen Datensatz begrenzen. Sie benötigen zustandsspezifische Daten pro Datensatz statt globaler Schlüssel? Fügen Sie vom Store-Objekt eine Relation zum Zielobjekt hinzu, anstatt die ID in den Schlüssel zu kodieren.
Dies ist eine Konvention, keine separate Funktion – der „KV Store“ ist einfach ein reguläres benutzerdefiniertes Objekt, das Sie definieren und mit der Standard-API abfragen. Das bedeutet, er profitiert von derselben Synchronisierung, denselben Berechtigungen und denselben Tools wie die übrigen Daten Ihrer App.