The space export format

This is the developer reference for Wodo's space export format. If you just want to export or import a space, read Import and export instead — this page is for people writing tools against the format: converters from other trackers, auditing scripts, or their own backup tooling.

The format identifier is wodo-space-export-v2. The format evolves additively: new fields may appear, existing fields keep their meaning, and anything optional here may be absent in older files. An importer should ignore fields it doesn't recognize.

The two artifacts

A space exports two ways, and the import wizard accepts either:

The two artifacts are self-describing and may be produced at different moments. Tools must tolerate asymmetry: JSON attachment entries missing from the ZIP (report and skip), and ZIP entries not referenced by the JSON (ignore).

Conventions

Top level

{
  "format": "wodo-space-export-v2",
  "exported_at": "2026-06-11T09:00:00Z",
  "space": { … },
  "labels": { … },
  "milestones": { … },
  "cycle_config": { … },
  "cycles": [ … ],
  "archive_config": { … },
  "views": { … },
  "items": [ … ],
  "documents": [ … ],
  "attachments": [ … ],
  "users": [ … ],
  "teams": [ … ],
  "overview_text": "…",
  "overview_yjs": "…"
}
FieldPresenceMeaning
formatalways"wodo-space-export-v2"
exported_atalwaysexport timestamp
spacealwaysspace metadata
labelsalwayslabel configuration
milestonesalwaysmilestone definitions, absolute deadlines
cycle_configoptionalrecurring-cycle configuration
cyclesalways (may be empty)concrete cycle instances with dates
archive_configoptionalauto-archive configuration
viewsalwayssaved views
itemsalwaysall items, including archived and deep-archived
documentsalwaysall documents
attachmentsalwaysattachment metadata (join key to the ZIP)
usersalwayssnapshot of every referenced user
teamsalwayssnapshot of every referenced team
overview_text / overview_yjsoptionalthe space overview, dual form

space

FieldTypeNotes
idUUIDthe source space
namestring
slugstring
regionstringregion the space lived in
short_id_prefixstring, optionale.g. "PROJ"
short_id_visiblebool
created_attimestamp

labels

{
  "order": ["<label_id>", …],
  "primary_label_id": "<label_id>" | null,
  "definitions": {
    "<label_id>": {
      "id": "…", "name": "Status", "description": "", "icon": "",
      "values_order": ["<value_id>", …],
      "values": {
        "<value_id>": {
          "id": "…", "name": "Done", "color": "#22c55e",
          "is_completion_state": true,
          "completion_prompt": "What shipped?"
        }
      }
    }
  }
}

Items hold one value per label (items[].labels maps label id → value id). A value with is_completion_state: true counts as "done" for that label; its optional completion_prompt is the question shown when an item reaches that state.

Deprecated labels and label values are included — items may still reference them — carrying three optional fields on the definition: deprecated (bool), deprecated_at (timestamp), and deprecated_by (user UUID, resolved against users).

milestones

order (array of ids) plus definitions keyed by id:

FieldTypeNotes
id, name
descriptionstring, optional
deadlinedate, optionalabsolute
deprecatedbooldeprecated milestones are included

cycles and cycle_config

Each entry in cycles is a concrete instance: { "id", "name", "start_date", "end_date", "archived" }.

cycle_config, when present: { "enabled", "pattern", "start_day", "prefix", "generate_ahead", "retain_past" }.

archive_config

{ "migration_days": <int> } — days after which archived items migrate to the deep archive.

views

order plus definitions keyed by view id:

FieldTypeNotes
id, name
content_typestring"items" or "docs"
view_typestring"board", "table", or "timeline"
column_grouping / row_groupingstring, optionala label id, "milestone", or "cycle"
sort_orderstring"0" default, "1" reversed, "da"/"dd" due date asc/desc
show_archivedbool
filtersstring, optionalencoded filter tokens, see below
zoom_levelstring, optionaltimeline only: "d", "w", "m", "q"

Filter encoding. filters is a comma-separated token list. UUID-bearing tokens use the raw 16 UUID bytes encoded base64url without padding:

TokenMeaning
{label}.{value}label value filter (two encoded UUIDs)
u:{user}assigned user
t:{team}assigned team
m:{milestone}milestone
cy:{cycle}cycle
d:overdue d:today d:soon d:has d:nonedue date
p:has p:nonehas/has no parent
bl:yes bl:noblocked state

items

FieldTypeNotes
idUUIDpreserved on import
short_idint, optionalthe number in PROJ-123; preserved on import
titlestring
description_text / description_yjsoptionaldual form
labelsobjectlabel id → value id
assignee_user_ids / assignee_team_idsarrays of UUIDsresolve against users / teams
due_date / start_datedate, optional
milestone_id / cycle_idUUID, optional
parent_idUUID, optionalsub-item relationship
blocked_byarray of UUIDsitems blocking this one
duplicate_ofUUID, optional
archivedbool
deep_archivedboolitem lives in the deep archive
created_at / updated_attimestamp, optionalpreserved on import
created_byUUID, optionalresolve against users; preserved on import via the people mapping
archived_attimestamp, optionalwhen archived; preserved on import (an old value means the deep-archive clock keeps running honestly)
completion_promptstring, optionalsnapshot of the prompt at the moment the item was completed
completion_note_text / completion_note_yjsoptionalthe completion note, dual form
commentsarraysee below

comments

Per item, in thread order:

FieldTypeNotes
idUUIDpreserved on import
author_idUUIDresolve against users
author_namestring, optionaldisplay-name snapshot at export time
content_text / content_yjsoptionaldual form
created_at / edited_attimestamp, optional
deletedboolsoft-deleted comments keep their content
deleted_attimestamp, optional
parent_idUUID, optionalreply threading

documents

FieldTypeNotes
idUUIDregenerated on import
titlestring
labelsobjectlabel id → value id
content_text / content_yjsoptionaldual form; content_yjs is the full Yjs doc
archivedbool
archived_attimestamp, optional
owner_user_ids / owner_team_idsarrays of UUIDsdocument owners; resolve against users / teams
parent_item_idUUID, optionalanchor item (at most one of the two parents)
parent_milestone_idUUID, optionalanchor milestone
review_cadence_daysint, optionalreview reminder cadence
last_reviewed_attimestamp, optionalexported honestly — a restore doesn't pretend documents were just reviewed
is_templatebooldocument lives in the Templates tab
forked_fromUUID, optionallineage to another document in this export
created_at / created_by / updated_atoptionalprovenance, as on items

attachments

Metadata for every attachment row, whether or not the ZIP was downloaded:

FieldTypeNotes
idUUIDjoin key to attachments/{id}/… in the ZIP
filenamestring
content_typestring
size_bytesint
uploaded_byUUIDresolve against users
uploaded_attimestamp
document_idUUID, optionalset when attached to a document
orphanedboolno longer referenced; pending deletion

Rich-text content references attachments by URL paths containing the attachment UUID; the import rewrites these inside the Yjs blobs.

users and teams

Snapshots of every user and team referenced anywhere in the export:

"users": [
  { "id": "…", "display_name": "Alice Anders", "email": "alice@example.com" }
],
"teams": [ { "id": "…", "name": "Platform" } ]

email is the cross-instance join key for import mapping; tombstoned accounts export as "Former user" without an email, and hard-deleted users are absent (importers fall back to the per-comment author_name snapshot).

How import treats the format

Current fidelity limitations

Known gaps in the format today — tracked, and the list shrinks as the format evolves: