Skip to main content
Logic functions run sandboxed in short-lived Node.js processes — once a run finishes, nothing it kept in memory survives. When you need to remember something between runs (cache an expensive API response, store a cursor for incremental syncs, debounce work, or hand state from one function to another), persist it in the built-in key-value store. Every application gets its own isolated namespace: entries are keyed by the authenticated app, so your keys can never collide with — or be read by — another application.

Get, set, delete

Import kv from twenty-sdk/logic-function. Values can be any JSON-serializable payload.
src/logic-functions/sync-linear-issues.ts

Scopes

Each entry has a scope, passed as an option on every call. The default is WORKSPACE.
  • WORKSPACE (default) — the entry is private to the current workspace install of your app. Each workspace that installs the app gets its own independent set of keys. This is what you want for caches, cursors, and per-workspace state.
  • SERVER — the entry is shared across every install of your app on the server. Server entries behave like claims: the stored value is always the workspaceId that claimed the key (omit value on set to claim the key for the current workspace), and only that workspace can overwrite or delete it. Any install can read the entry.
Server claims exist for cross-workspace routing. A server-route resolver runs in the application registration owner workspace, but an inbound webhook usually only carries an external account id — not a Twenty workspaceId. Have each workspace claim its external id at connect time, then resolve it in the route:
Because a server key can only be claimed for the caller’s own workspace and never overwritten by another one, a workspace can’t hijack a mapping that belongs to someone else. kv.set throws when the key is already claimed by another workspace.

Use it: cache an expensive call

A typical use is caching a slow or rate-limited third-party response so repeated runs reuse it instead of paying the cost every time.
src/logic-functions/getExchangeRate.logic-function.ts

Patterns & tips

  • Namespacing. Prefix keys to keep different concerns apart — sync-cursor:linear, cache:exchange-rate:USD:EUR, lock:nightly-report.
  • Expiry (TTL). The store has no built-in expiration. Store a timestamp inside the value (as in the cache example) and check it on read, or clear stale keys from a cron-triggered function.
  • What to store. Any JSON-serializable value — numbers, strings, arrays, objects. Keep entries small; this is for coordination and caching, not large blobs or files. For files, use a FILES field and uploadFile.
  • Visibility. Entries live in the instance database, not as workspace records — they never show up in the workspace UI, aren’t part of your app’s data model, and need no role or object permissions.

Alternative: a queryable store object

The built-in store is deliberately opaque: entries aren’t records, so you can’t browse them in the UI, relate them to other objects, or filter them with record queries. When you need any of that — say a visible sync log, or per-record state — define a small technical object with a unique key field and a RAW_JSON value field instead, and query it through the typed API client. See Objects for the defineObject reference and Data → Unique indexes for enforcing key uniqueness.
  • Scoping to a record. Add a relation from the store object to the target object rather than encoding the id into the key.
  • Visibility & permissions. Rows live in the workspace database like any other record, so they’re queryable through the API and respect your app’s role. To keep the store out of the main UI, leave it off your navigation menu.
Unlike the built-in store, a custom object is always scoped to one workspace — it can’t share entries across installs the way SERVER keys do.