Data Retention
Axiom deletes old data with scheduled cleanup jobs in app/src/main/java/io/apitomy/axiom/app/. The
retention periods live in the single-row retention_config table and are edited on the Data Retention
page (/data-retention) or with GET/PUT /api/v1/system/retention.
The rule for every cleanup job is: after it runs, no row points at a deleted row, except for the documented soft references listed under Accepted dangling references.
Settings
| Column | API field | Default | Cleanup job |
|---|---|---|---|
closed_project_retention_days |
closedProjectRetentionDays |
90 | ClosedProjectCleanup |
trace_retention_days |
traceRetentionDays |
30 | TraceCleanup |
event_retention_days |
eventRetentionDays |
90 | StreamEventCleanup |
scheduled_job_run_retention_days |
scheduledJobRunRetentionDays |
0 | HistoryCleanup |
report_retention_days |
reportRetentionDays |
0 | HistoryCleanup |
workflow_run_retention_days |
workflowRunRetentionDays |
0 | HistoryCleanup |
activity_log_retention_days |
activityLogRetentionDays |
0 | HistoryCleanup |
ai_usage_retention_days |
aiUsageRetentionDays |
0 | HistoryCleanup |
The five history settings were added in V72 (#429). For them, 0 means keep forever, and 0 is the
default, so upgrading never deletes data unexpectedly. Negative values are rejected with 400. The three
original settings must be at least 1, because 0 would delete everything; smaller values are also rejected with
400. A PUT that omits any setting (or sends null) leaves its current value unchanged. V72 also dropped
the unused event_source_log_retention_days column.
Batching and Retries
Every job runs hourly (staggered with delayed), selects at most 500 IDs per transaction, and deletes by
those IDs or with sub-selects, never with an unbounded IN list. Each batch runs in its own transaction
through CleanupRetry, which retries database lock failures. Each job has a package-private doCleanup()
that runs all batches in the caller's transaction; tests use it.
What Each Job Does
TraceCleanup
Deletes traces whose started_on is older than the retention period, with their trace_node and
tool_execution rows. Before deleting, it clears (sets to NULL):
trace_idontask,scheduled_job_run,report,workflow_run,routing_outcome,activity_logandai_usage;triggered_by_trace_idonscheduled_job_runandreport;trace_node_idonrouting_outcome_itemandworkflow_run_resume, for the deleted trace's nodes.
TraceCleanupTest.everyTraceColumnIsHandled scans every entity for columns named trace_id, *_trace_id
or trace_node_id and fails if one is not in its list of handled columns. When you add such a column, clear
it in TraceCleanup, add it to the assertions in TraceCleanupTest, and add it to the list.
StreamEventCleanup
Deletes stream events older than the retention period, with their ledger entries, routing outcomes and
routing outcome items. An event is kept while a workflow run with status running or waiting:
- has it as
trigger_event_id, or - has a
workflow_run_resumerow with thatevent_id.
Such an event becomes eligible on the first cleanup after the run finishes, so the run → event link is never lost while the run can still use it. The job also deletes connection poll logs older than 3 days.
ClosedProjectCleanup / ProjectDeletionService
Deletes Completed projects whose updated_on is older than the retention period (the same code runs when a
project is deleted through the API). It deletes the project's thread entries, AI usage, activity log entries,
tasks, workflow runs and their waits, event subscriptions and resume rows, labels and workspace.
Traces and routing outcomes are event history, not project data, so they are kept and their references
are cleared: trace.project_id, routing_outcome.project_id and task_id, and routing_outcome_item
project_id, task_id and workflow_run_id. Configuration versions are not project-scoped.
HistoryCleanup
Each setting is skipped when it is 0.
| Data | Deleted rows |
|---|---|
| Scheduled job runs | Status Completed or Failed and created_on older than the period |
| Reports | Status Completed or Failed and created_on older than the period, with labels |
| Workflow runs | Status completed, failed or cancelled, and completed_on (or started_on if unset) older than the period |
| Activity log | created_on older than the period, with or without a project |
| AI usage | created_on older than the period, with or without a project |
Workflow runs use an allow-list of finished statuses (FINISHED_WORKFLOW_RUN_STATUSES), so runs in a status
added later are kept until the list is updated. Deleting a workflow run deletes its workflow_wait,
workflow_event_subscription and workflow_run_resume rows and clears task.workflow_run_id and
routing_outcome_item.workflow_run_id. Tasks are kept: they belong to the project and are deleted with it.
The run's trace follows trace retention.
Deleting a job run or report does not touch its configuration version (scheduled_job_version /
report_definition_version), which belongs to the definition.
Concurrency
The cleanup jobs use bulk JPQL UPDATE and DELETE statements, which bypass the persistence context. If
another transaction has the same row loaded as a managed entity (for example a task whose trace_id is being
cleared) and flushes it after the cleanup commits, it can write the old value back, or fail on a row that no
longer exists. The window is small, because cleanup only touches rows that are past their retention period,
and this was already true before #429. The next cleanup run clears any value that was written back.
Accepted Dangling References
These columns are soft references with no foreign key. They are kept on purpose so history survives, and consumers must treat them as possibly dangling:
scheduled_job_run_id,report_idandreport_definition_idonai_usageandactivity_log, andtrace.report_id, after a job run, report or definition is deleted (see Correlation).event_idontrace,task,activity_log,ai_usageandworkflow_run_resume,ledger_idonworkflow_run_resume, andtrigger_event_id/trigger_ledger_idonworkflow_run, after event retention deletes the event of a finished run (see Tracing and Event System Design).trace_node.entity_type/entity_id, which are free-form display references.