Saltar al contenido principal
Las funciones de lógica se ejecutan aisladas en procesos de Node.js de corta duración; una vez que una ejecución finaliza, nada de lo que se mantuvo en memoria sobrevive. Cuando necesitas recordar algo entre ejecuciones (almacenar en caché una respuesta de API costosa, guardar un cursor para sincronizaciones incrementales, aplicar debounce al trabajo o traspasar estado de una función a otra), persístelo en la base de datos del espacio de trabajo. No necesitas una primitiva de almacenamiento dedicada para esto: un pequeño objeto técnico con un campo key y un campo value te ofrece un almacén de clave-valor duradero, con alcance al espacio de trabajo, consultable a través del mismo cliente de API tipado que ya utilizas para los registros.
  ┌─────────────────┐   set(key, value)    ┌──────────────────────────┐
  │ Logic function  │ ───────────────────▶ │ "KV Store" object        │
  │ (your handler)  │ ◀─────────────────── │  key (unique)  │  value  │
  └─────────────────┘   get(key)           └──────────────────────────┘

Definir el objeto de almacenamiento

Declara un objeto personalizado con dos campos: key (un TEXT único) y value (un RAW_JSON para que puedas almacenar cualquier carga útil serializable en JSON). Consulta Objetos para ver la referencia 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 la unicidad de la clave

Añade un índice único en key para que la misma clave nunca pueda tener dos filas. Esta es la primitiva recomendada para la unicidad; consulta Datos → Índices únicos.
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,
    },
  ],
});

Leer y escribir desde una función de lógica

Envuelve el objeto tras unos pequeños helpers para que el resto de tu código se lea como una API de clave-valor: get, set y del. Utilizan CoreApiClient, que se genera a partir del esquema de tu espacio de trabajo y está completamente tipado contra el 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 },
    });
  }
};
El índice único protege contra duplicados, pero dos ejecuciones que escriben la misma clave nueva en el mismo instante aún pueden competir entre la búsqueda y la creación. Trata una creación que falle por la restricción de unicidad como “alguien más ganó”: captúrala y vuelve a leer, o inténtalo de nuevo como una actualización.

Úsalo: almacena en caché una llamada costosa

Un uso típico es almacenar en caché una respuesta lenta o limitada por rate limiting de un tercero para que ejecuciones repetidas la reutilicen en lugar de pagar el coste cada vez.
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,
});

Patrones y consejos

  • Espacios de nombres. Añade un prefijo a las claves para mantener separadas las distintas responsabilidades y facilitar las búsquedas masivas: sync-cursor:linear, cache:exchange-rate:USD:EUR, lock:nightly-report. Filtra con key: { like: 'cache:%' } para listar o limpiar todo un espacio de nombres.
  • Caducidad (TTL). El almacén no tiene caducidad integrada. Almacena una marca de tiempo dentro de value (como en el ejemplo de caché) y revísala al leer, o añade un campo DATE_TIME y borra periódicamente las filas obsoletas desde una función activada por cron.
  • Qué almacenar. RAW_JSON contiene cualquier valor serializable en JSON: números, cadenas, arreglos, objetos. Mantén las entradas pequeñas; esto es para coordinación y almacenamiento en caché, no para blobs grandes o archivos. Para archivos, utiliza un campo FILES y uploadFile.
  • Visibilidad y permisos. Las filas residen en la base de datos del espacio de trabajo como cualquier otro registro, por lo que se pueden consultar a través de la API y respetan el rol de tu aplicación. Para mantener el almacén fuera de la interfaz principal, déjalo fuera de tu menú de navegación.
  • Ámbito por registro. ¿Necesitas estado por registro en lugar de claves globales? Añade una relación desde el objeto de almacenamiento al objeto de destino en lugar de codificar el id en la clave.
Esto es una convención, no una característica independiente: el “KV Store” es solo un objeto personalizado normal que defines y consultas con la API estándar. Eso significa que se beneficia de la misma sincronización, permisos y herramientas que el resto de los datos de tu aplicación.