Skip to content

Conversation retention and deletion (Löschkonzept)

Myra AI Workspace can automatically and permanently delete a tenant's chat content once it exceeds a configurable retention period (a Löschfrist). This is the Lösch- und Archivkonzept for user-generated conversation data: it names what is deleted, when, who is responsible, and how each deletion is recorded.

⚠️ Caution: Retention deletion is irreversible. A purged conversation and everything it contains cannot be recovered. Retention is therefore disabled by default and must be explicitly enabled per tenant.


What the retention job deletes

When a tenant has a retention period configured, a background job periodically and permanently deletes every conversation of that tenant whose last activity is older than the retention period — together with everything the conversation owns:

  • the conversation record and its messages;
  • all attachments and generated images on those messages;
  • conversation summaries and embeddings (the derived search index);
  • conversation feedback, shares, and any conversation-owned knowledge files;
  • pending file-upload claims tied to the conversation.

In addition, if a message from a purged conversation had been flagged through the content-report (moderation) flow, the reported message text is erased from the moderation record. The moderation record itself is kept (its status, reason, and timestamps carry no conversation content) so the moderation trail is not lost.

Retention basis: last activity

The deadline is measured from a conversation's last activity, not its creation. A conversation's clock is reset whenever a new message is added to it, so a conversation that is still in use is never purged. Concretely: a 365-day retention period deletes only conversations that have had no activity for 365 days.


Configuring the retention period

Required role: platform admin — in the UI (the Tenants list is admin-only) and on the API: a tenant admin's PATCH /admin/v1/tenants/:id that would change conversation_retention_days (or request_log_retention_days) is rejected 403 and writes nothing. The Löschfrist is set by the platform operator on the tenant's behalf.

In the admin console, open User Management → Tenants → (a tenant) → Edit (platform admin only) and set Conversation retention (days). Leave the field blank to disable retention; enter a whole number of days between 30 and 3650 to arm it. The tenant detail page shows the current setting (the number of days, or Disabled) at a glance. Every change to the setting is recorded in the audit log.

The same value can be set programmatically, in days, via the tenant update API:

PATCH /admin/v1/tenants/{id}
{ "conversation_retention_days": 365 }
Value Effect
null or 0 Disabled (the default). No conversation is ever deleted by retention.
30 – 3650 Delete conversations with no activity for that many days (30 days to 10 years).
any other value Rejected with 400 (see below).

Read the current value back from GET /admin/v1/tenants/{id} (conversation_retention_days).

Accepted input and what is rejected

The field is validated at the trust boundary and fails closed:

  • The value must be a whole number. A fractional value such as 0.4 or 45.5 is rejected with 400 — it is not rounded.
  • A non-zero value below 30 or above 3650 is rejected with 400. The 30-day floor prevents a mistyped 1 (or an hours/days unit mix-up) from purging a tenant's recent history.
  • A string ("365"), array, boolean, or any non-numeric type is rejected with 400.
  • 0 and JSON null clear the setting back to disabled.

A rejected request never changes the stored value.


The deletion log (Löschprotokoll)

Every retention sweep that deletes anything writes one entry to the audit log per tenant, so each automated deletion is accountable (Nachvollziehbarkeit). The entry records counts only — how many conversations, messages, and attachments were deleted and how many moderation records were scrubbed — together with the retention period and the cutoff timestamp that were applied. No conversation content is ever written to the log.

The entry is attributed to the system actor (the automated job has no human operator) with the action conversation.retention_purged. When a tenant has more expired conversations than one sweep processes, the entry records truncated: true and batches_run so a reader can tell a fully-completed purge from one whose remainder is deleted on the next run.


What this job does NOT delete

The retention job targets conversation content (the conversation, its messages, attachments, and the derived stores listed above). Some related records live in separate operational stores with their own lifecycle and are not removed by this job. They are named here so the concept is complete:

Store Contains Handling
Request / inference log The raw prompt and model response of each API call Kept for cost attribution, analytics, and SIEM export; not conversation-scoped. Erased when a user exercises the right to erasure. A dedicated per-tenant request-log retention Löschfrist is a separate control from conversation retention.
Partner / affiliate account An affiliate partner's name and login email (partner), plus their partner_login one-time-passcode rows Not tenant- or user-scoped (a top-level entity). Suspending a partner preserves history; erasure (DELETE /admin/v1/partners/{id}/erase, Art. 17) removes the partner row and its OTP rows, nulls every attribution that referenced it (tenant.referred_by_partner_id/referred_at, signup_intent.referred_by_partner_id), and records a counts-only partner.erased audit tombstone (no name/email). Referred-tenant visibility: a partner can see its own referred tenants' identity (company, admin name/email), status, seats and usage to coordinate — this is read-through (no copy is stored on the partner), so a referred customer's own erasure removes it from the partner view automatically, and a partner sees only its own referred tenants. Legal footing (affiliate agreement + T&C/DPA) tracked internally. See the Partner dashboard API.
Model-error / triage queue Diagnostic detail of a failed turn (may include a derived snippet) and a conversation reference Kept for operator triage; the conversation reference is left dangling after a purge. Erased on user erasure.
Distributed traces Step-level trace of a gateway request Governed by Trace retention (default 48 hours — far shorter than any conversation Löschfrist).
Cost ledger rows Per-leg token/cost accounting No content; the conversation reference is left dangling after a purge.
Client error reports Browser-reported errors from the app (message, stack, URL, user agent — may contain user-visible text) Kept 30 days (fixed), then removed by an hourly, batched sweep; the timestamp is the server's receive time. No per-user dimension — not erased per user (the aggregate triage item survives without the raw text). See the client-errors API.
Model deprecation probe log Outcome of the hourly model-availability probe per provider/model (no personal data) Kept 30 days (fixed), then removed by the same hourly sweep.
Health probe log Outcome of the synthetic health probes (table names, counts, pass/fail — no personal data) Kept 30 days (fixed), then removed by the same hourly sweep.
Workflow run outputs Per-step work-product output of a workflow run (run_state) and the submitter's trigger payload (trigger_input) Retained as tenant operational work-product under a distinct legal basis (GDPR Art. 6(1)(f)); not conversation-scoped. On user erasure the run row is kept and the erased user's identifiers (uuid and email) are scrubbed in place from run_state, while the submitter's own trigger_input is nulled on their own runs — see the erasure section below. The retention criterion is the operational life of the workflow/tenant, bounded by the workflow-run retention ceiling (storage limitation, Art. 5(1)(e)).
Workflow artifacts Binary files a workflow node produces or fetches (e.g. a document, a filled PDF), stored against the run that made them (workflow_artifact) Retained as tenant operational work-product on the same legal basis as run outputs. Because the bytes are binary, they cannot be identifier-scrubbed in place like run_state JSON: on the owner's erasure the artifacts of that user's own runs are deleted outright, and on tenant deletion all of the tenant's artifacts are deleted. They also cascade automatically when their run is removed by the workflow-run retention ceiling. Residual: an artifact attached to another tenant member's run that happens to embed the erased user's personal data is not auto-deleted (a binary cannot be selectively scrubbed and deleting a colleague's work-product would be destructive) — the same bound the run-output free-text scrub already carries.
Reactivation-mail send log One row per member-reactivation e-mail step claimed for a member (user_reactivation_event: sequence, step, when, outcome, whether activity followed) — no message content Kept for the life of the member's seat: it is the record of what was mailed to whom (and the basis of the reactivation metrics). Erased on the right to erasure (Art. 17, FK cascade from the user row; the erased member's id is also cleared from any seat they invited) and on tenant purge; exported in the tenant exit export (reactivation_events) and the member's own Art. 15 export. The member's opt-out stamp (reactivation_opted_out_at) lives on the user row and follows it.
Signup-reminder send log One email-keyed row per signup-abandonment reminder attempted (signup_reminder: the address, when claimed/sent, outcome) — no message content. Pre-payment state, no account Reaped at 90 days (the "already contacted" dedup window; after it a genuinely new abandonment can be re-reminded). Also erased by e-mail on a member's Art. 17 hard-delete and on tenant purge. For a non-account abandoner (the common case, no user row), a pre-90-day erasure request is served by the 90-day reaper or a manual DPO one-row delete.
Signup-reminder suppression One email-keyed row per unsubscribe from signup reminders (signup_reminder_suppression: the address, when, source) Retained as an Art. 21 objection (do-not-contact) record — deleting it would re-enable mailing the person, so it outlives the signup intent and is NOT reaped and NOT removed on account erasure/tenant purge; a removal-on-request is a deliberate manual DPO action.
Replay-record store The raw (unmasked) per-turn replay record — the client messages, intra-turn tool calls/results, the available tool set, and the sampling params — captured only when the tenant opts in (replay_capture_enabled, default off) so a turn can be faithfully re-run through the gateway with a different model for offline evaluation. Not captured on guardrail-blocked turns. Bounded by a TTL (replay_capture_ttl_hours, default 48 h, reaped hourly) and a per-tenant+gateway cap (replay_capture_cap, default 200); residency-tagged; erased on the right to erasure (Art. 17 — by both the turn owner and the run-as driver) and on tenant purge. It is a raw store — enable it only where that retention is acceptable. See replay_capture_enabled.

If your compliance requirement is that no trace of a conversation's content survives past the Löschfrist anywhere, address the request/inference log with its own request-log retention Löschfrist, and raise the remaining operational stores with your operator.


Responsibilities and defaults

Responsibility Owner
Deciding the retention period for a tenant Platform admin (on the tenant's instruction; tenant admins cannot set it themselves)
Enabling the retention capability for the deployment Platform operator
Running the automated deletion job Myra AI Workspace (background job)
Recording each deletion Myra AI Workspace (audit log)

Two independent switches must both be on before any conversation is deleted:

  1. The platform operator must enable the capability for the deployment (it ships disabled — see the operator note below).
  2. A platform admin must set a non-zero retention period for the tenant.

If either switch is off, nothing is deleted.


Retention scope: org, workspace, and user (ceiling precedence)

Conversation retention can be set at three scopes, from widest to narrowest:

Scope Where it is set Field
Organisation (tenant) User Management → Tenants → (a tenant) → Edit (admin only) conversation_retention_days on PATCH /admin/v1/tenants/:id
Workspace (project) project settings (owner / admin only) conversation_retention_days on PATCH /admin/v1/projects/:id
User User Management → Users → (a user) → Edit conversation_retention_days on PATCH /admin/v1/users/:id

Each is an independent whole number 30–3650 (or null/0 = not set at that scope); out-of-range values are rejected with 400.

Ceiling precedence — a narrower scope may only SHORTEN retention, never lengthen it. A conversation is deleted once it is older than the shortest frist that applies to it (its user's, its workspace's, or the org's — whichever are set). So a user- or workspace-level frist can delete a conversation sooner than the org policy, but can never keep it longer than the org maximum. Example: org = 30 days, a user = 90 days → that user's conversations are still deleted at 30 days (the org ceiling wins). If no scope sets a frist, the conversation is never deleted (opt-in).

Setting a workspace frist is restricted to the project owner (or a platform admin), since it arms irreversible deletion over other members' conversations. Every change is audited; each automated sweep writes a per-scope Löschprotokoll audit row (conversation.retention_purged, tagged with the scope and the owning tenant).


Request-log retention

Chat conversations and request logs are retained independently. The request log (request_log) stores the prompt and response of each inference request; a per-tenant Löschfrist permanently deletes rows older than a configured number of days.

Required role: platform admin (the Tenants list is admin-only). In User Management → Tenants → (a tenant) → Edit, set Request-log retention (days) — blank to disable, or a whole number 30–3650. The tenant detail page shows the current setting. The same value is settable via the API:

PATCH /admin/v1/tenants/{id}
{ "request_log_retention_days": 365 }
Value Effect
null or 0 Disabled (the default). No request log is ever deleted by retention.
30 – 3650 Permanently delete request_log rows older than that many days.
any other value Rejected with 400.

The same trust-boundary validation as conversation retention applies (whole number, 30–3650, else 400; the client is never the authority). Two independent switches gate deletion: a deployment-wide switch controlled by your operator (off by default) and a non-zero per-tenant frist. Each change to the setting is audited; each sweep writes a counts-only Löschprotokoll audit row (request_log.retention_purged).

The cost ledger is preserved. Deletion removes only the request_log row (the content). The append-only request_log_legs accounting ledger — token counts, cost, model, region — is kept (it carries no prompt/response content and has its own, longer, accounting-retention basis).


Scope and fail-safe behaviour

By design, the retention job never deletes more than intended:

  • A conversation whose last activity is exactly at the cutoff is kept (the boundary is strict).
  • A tenant with no retention period, or 0, is skipped entirely.
  • A tenant that has itself been deleted is skipped; its conversations are handled by tenant deletion, not by retention.
  • The job runs in bounded batches, so a first sweep over a large backlog cannot lock the database; it resumes on the next run until the backlog is cleared.

Retention deletion is distinct from, and complementary to, the retention of observability data (see Trace retention) and a user's right-to-erasure request, which deletes a single user's data on demand.

Workflow-run retention

Workflow-run outputs (workflow_run.run_state, the submitter's trigger_input, and the per-step workflow_step_run outputs) are retained tenant operational work-product (GDPR Art. 6(1)(f)) — they survive a single user's right-to-erasure (identity-scrubbed, not content-deleted). To satisfy storage limitation (GDPR Art. 5(1)(e)), that retention is bounded by an age-based ceiling: a worker-0 background sweep deletes any terminal workflow run (succeeded, failed, cancelled) whose created_at is older than the window, cascading its workflow_step_run children. A run that is still in flight (pending, running) or awaiting approval (suspended) is never purged regardless of age.

  • Window: global, deployment-level, configured by your operator (default 365 days; every degenerate value clamps to ≥ 1 day, so the cutoff can never reach recent runs). Like Trace retention it is a single global window — a per-tenant clock cannot drive one global delete.
  • Cadence & safety: it rides the same hourly worker-0 maintenance sweep as trace retention, in bounded batches (a first sweep over a backlog cannot long-lock the table; it resumes next tick), and is best-effort (a DB fault logs a warning and is retried, never crashing the timer).
  • Distinct from erasure and tenant deletion: erasure keeps the row and scrubs identifiers; whole-tenant deletion removes all of a tenant's runs on decommission; this ceiling removes old terminal runs of every tenant once retention lapses.
  • Workflow artifacts (workflow_artifact, the binary files a run produces) are stored in-row against their run with a foreign key that cascades on run delete, so all three paths reap the bytes: the retention ceiling (run purged → artifacts cascade), tenant deletion (explicit tenant-scoped delete), and user erasure. Unlike run_state, a binary artifact cannot be identifier-scrubbed, so on erasure the erased user's own runs' artifacts are deleted, not scrubbed (the foreign-run residual noted in the table above).

Right-to-erasure is completeness-guarded. Most tables reference a user through a foreign key, so the user-row delete cascades. Columns that reference a user without a foreign key (an editorship/actor id such as shared_by, started_by, triaged_by_id) are anonymized, deleted, or re-assigned explicitly — a nullable reference is set to null, an ephemeral row (e.g. a live-session) is deleted, and a not-null infrastructure owner (the chat-bridge connection) is re-assigned to a surviving tenant admin so one erasure never tears down a whole tenant's integration. A user id embedded inside a JSON value (a comment's @mention list, a workflow's approver snapshot) is scrubbed in place. Personal data held in a record keyed by email rather than by user id — the pre-payment signup record (signup_intent: email, name, company, phone, job title, capture IP, consent snapshot) and the email one-time-code challenge — is deleted by that email on erasure; because these tables carry no foreign key to user, they are reached by an explicit email-keyed delete (the same delete the tenant purge applies), not by the cascade, and the drift-sweep below does not see them, so their coverage is held by an integration test instead. This registration data is also returned to the person in their Art. 15 self-service export. Because the link is the address itself, an email-keyed record captured under an address that was later administratively rectified (Art. 16) is reconciled by the email-change operational procedure rather than by these automatic email deletes — a single, deliberate caveat of the email-keyed class (it applies equally to the tenant purge and the email one-time-code challenge). A workflow run's output (workflow_run.run_state) is a special case of scrub-in-place: it is a tenant operational work-product record retained under a distinct legal basis, so it is not content-deleted like an inference log — instead the erased user's uuid and email are scrubbed to a tombstone while the work-product itself is retained; the run's own submitter input (trigger_input) is nulled on the subject's runs and identity-scrubbed on runs owned by others. Because the retained work-product is free-form, free-text personal data the user may have typed or that a model may have produced can persist inside a retained run_state — the scrub removes the structured identifiers (uuid, email), not every mention of a person; this is the deliberate consequence of retaining the output as tenant work-product under Art. 6(1)(f). The retained work-product's storage-limitation ceiling is enforced by the workflow-run retention ceiling above. A schema drift-sweep runs at build time and fails the build when a new no-foreign-key user-reference column — one named with a recognized actor shape (*_by, *_by_id, *_user_id, or a curated set of actor-id nouns) — is added without erasure handling, so this completeness class does not silently regress.

One deliberate residual, stated because it is a consequence of a safety control rather than an oversight: when an erasure hard-deletes a knowledge area (a project with no other member), an agent of a different user that was bound to it keeps the area's opaque id as an asserted binding, so the agent can go on failing closed instead of silently losing the residency restriction that area carried — see Knowledge areas. What is retained is the project's identifier, never its name, its content or its members; the project row and everything it owned are deleted. The owner clears the leftover id by saving the agent's knowledge areas, which is also what the product prompts them to do.

Audit-log retention

The audit log (audit_log) — the durable record of every mutating admin action and every security event — has its own global retention horizon, floored at six months. This is a single deployment-level window (a per-tenant clock cannot drive one global, chain-aware delete), configured by your operator:

Value Effect
unset Default 365 days.
< 180 (a numeric 0 or negative) Clamped up to the 180-day floor — the six-month minimum can never be undercut.
Non-numeric or non-finite (e.g. "abc", 1e999, NaN) Treated as unset → 365 days (still never below the 180-day floor).
180–3650 Retain audit events for that many days.
> 3650 Clamped down to the 3650-day (10-year) ceiling.

Retention is chain-aware (never breaks tamper-evidence)

Audit-log retention is coupled to the tamper-evidence hash-chain (see Tamper-evidence) by design: the pruner deletes only whole checkpointed ranges, oldest first, and never the most recent range. A retained event therefore always still chains from a trusted checkpoint anchor, so integrity verification keeps working across the pruning boundary. Each pruning run writes a counts-only Löschprotokoll audit row (audit_log.retention_purged) — never content — which is itself sealed into the chain, so the deletion is accountable and tamper-evident.

Coupling. Because pruning is chain-aware, it runs only when the hash-chain is enabled. The chain is on by default wherever an audit-chain key is provisioned for your deployment (the master switch defaults to on when a key is present, off when it is not, and your operator can force it either way). So a keyed deployment enforces the retention horizon by default, while a keyless one leaves the audit log un-pruned. When a key is first provisioned on a previously-keyless deployment, sealing catches up on the historical backlog and pruning then begins draining rows older than the horizon incrementally (at least two checkpoints are required before any range is deleted) — so confirm the horizon is intended before enabling.