> ## Documentation Index
> Fetch the complete documentation index at: https://twenty-claude-cool-pascal-5ay683.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Хранилище ключ-значение

> Сохраняйте промежуточные результаты, кэшируйте данные и разделяйте состояние между запусками логических функций с помощью встроенного в приложение хранилища ключ-значение.

Логические функции выполняются в изолированной среде во временных процессах Node.js — после завершения запуска в памяти ничего не сохраняется. Когда вам нужно **что-то запомнить между запусками** (кэшировать дорогой ответ API, сохранить курсор для инкрементальной синхронизации, «задебаунсить» работу или передать состояние от одной функции к другой), сохраняйте это во встроенном хранилище ключ-значение.

Каждое приложение получает собственное изолированное пространство имён: записи привязаны к аутентифицированному приложению, поэтому ваши ключи никогда не могут конфликтовать с ключами другого приложения или быть прочитаны другим приложением.

```text theme={null}
  ┌─────────────────┐   kv.set(key, value)   ┌──────────────────────────┐
  │ Logic function  │ ─────────────────────▶ │ Application KV store     │
  │ (your handler)  │ ◀───────────────────── │  key (unique)  │  value  │
  └─────────────────┘   kv.get(key)          └──────────────────────────┘
```

## Получение, установка, удаление

Импортируйте `kv` из `twenty-sdk/logic-function`. Значения могут быть любыми JSON-сериализуемыми данными.

```ts src/logic-functions/sync-linear-issues.ts theme={null}
import { kv } from 'twenty-sdk/logic-function';

// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');

// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);

// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```

## Области действия

У каждой записи есть область видимости (scope), которая передаётся как опция при каждом вызове. По умолчанию используется `WORKSPACE`.

* **`WORKSPACE`** (по умолчанию) — запись доступна только для текущей установки вашего приложения в рабочем пространстве. Каждое рабочее пространство, в которое устанавливается приложение, получает собственный независимый набор ключей. Это то, что вам нужно для кэшей, курсоров и состояния на уровне рабочего пространства.
* **`SERVER`** — запись разделяется между **всеми установками** вашего приложения на сервере. Серверные записи ведут себя как механизм резервирования: сохраняемым значением всегда является workspaceId, зарезервировавший ключ (опустите `value` при вызове `set`, чтобы зарезервировать ключ для текущего рабочего пространства), и только это рабочее пространство может перезаписать или удалить запись. Любая установка может прочитать эту запись.

Резервирования на сервере используются для маршрутизации между рабочими пространствами. [Резолвер серверного маршрута](/l/ru/developers/extend/apps/logic/logic-functions#server-route-trigger) выполняется в рабочем пространстве владельца регистрации приложения, но входящий вебхук обычно содержит только внешний идентификатор аккаунта, а не workspaceId в Twenty. Пусть каждое рабочее пространство зарезервирует свой внешний идентификатор в момент подключения, а затем разрешайте его в маршруте:

```ts theme={null}
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });

// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
  scope: 'SERVER',
});
```

Поскольку серверный ключ может быть занят только для собственного рабочего пространства вызывающего кода и никогда не может быть перезаписан другим, рабочее пространство не может перехватить сопоставление, принадлежащее кому-то ещё. `kv.set` выбрасывает ошибку, если ключ уже занят другим рабочим пространством.

## Использование: кэширование дорогого вызова

Типичный сценарий — кэширование медленного или ограниченного по частоте ответа стороннего сервиса, чтобы при повторных запусках переиспользовать его, а не нести затраты каждый раз.

```ts src/logic-functions/getExchangeRate.logic-function.ts theme={null}
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';

const ONE_HOUR_MS = 60 * 60 * 1000;

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

const handler = async (params: { from: string; to: string }) => {
  const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
  const cached = await kv.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 kv.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,
});
```

## Шаблоны и советы

* **Пространства имён.** Добавляйте префиксы к ключам, чтобы разделять разные задачи — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Срок жизни (TTL).** У хранилища нет встроенного механизма истечения срока действия. Сохраняйте метку времени внутри значения (как в примере с кэшем) и проверяйте её при чтении или очищайте устаревшие ключи из [функции, запускаемой по cron](/l/ru/developers/extend/apps/logic/logic-functions).
* **Что хранить.** Любое JSON-сериализуемое значение — числа, строки, массивы, объекты. Держите записи небольшими; это для координации и кэширования, а не для больших блобов или файлов. Для файлов используйте поле `FILES` и [`uploadFile`](/l/ru/developers/extend/apps/logic/logic-functions#uploading-files).
* **Видимость.** Записи хранятся в базе данных экземпляра, а не как записи рабочего пространства — они никогда не отображаются в интерфейсе рабочего пространства, не являются частью модели данных вашего приложения и не требуют ролей или прав доступа к объектам.

## Альтернатива: объект-хранилище с возможностью запросов

Встроенное хранилище намеренно сделано непрозрачным: записи не являются записями объектов, поэтому вы не можете просматривать их в интерфейсе, связывать их с другими объектами или фильтровать их с помощью запросов к записям. Когда вам нужно что-то из этого — например, видимый журнал синхронизаций или состояние на уровне записи, — вместо этого определите небольшой **технический объект** с уникальным полем `key` и полем `value` типа `RAW_JSON`, и выполняйте к нему запросы через [типизированный API-клиент](/l/ru/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). См. [Objects](/l/ru/developers/extend/apps/data/objects) для справки по `defineObject` и [Data → Unique indexes](/l/ru/developers/extend/apps/data/overview#unique-indexes) по обеспечению уникальности ключей.

* **Привязка к записи.** Добавьте [relation](/l/ru/developers/extend/apps/data/relations) от объекта-хранилища к целевому объекту вместо кодирования идентификатора в ключе.
* **Видимость и разрешения.** Строки живут в базе данных рабочего пространства как любые другие записи, поэтому к ним можно обращаться через API, и они подчиняются [ролям](/l/ru/developers/extend/apps/config/roles) вашего приложения. Чтобы скрыть хранилище из основного пользовательского интерфейса, не добавляйте его в [навигационное меню](/l/ru/developers/extend/apps/layout/navigation-menu-items).

<Note>
  В отличие от встроенного хранилища, пользовательский объект всегда ограничен одним рабочим пространством — он не может разделять записи между установками так, как это делают ключи `SERVER`.
</Note>
