Tidenda

Concepts

Document formats

Article, planning item, newswire, image, workflow, AI operation - the shapes on the wire.

Document formats

Every editorial artefact in Tidenda - articles, planning items, newswire feeds, workflows themselves - is modelled as a document. This page describes the shapes integrators meet on the wire: in webhook payloads, in the public injection API, and in the snapshot JSON the platform signs on publish.

All documents share the same envelope: id, type, title, meta, content, links, and history. What changes between types is what lives under meta and content. Admin-internal document types (API keys, webhook configs, user records) are not documented here - they're not part of the integrator-facing surface.

The shapes below are sketches for quick reference. The runtime validates incoming documents against the canonical schemas — if your payload doesn't match, the response carries an INVALID_PAYLOAD error with the failure reason.

The shared envelope

Every document carries this envelope before its type-specific content.

{
  "id": "string",            // UUID, tenant-scoped
  "type": "string",          // e.g. "core/article"
  "title": "rich text",      // serialises to plain string in JSON
  "meta": {
    "priority": 3,           // 1-6, lower is more important
    "visibility": "internal", // or "public"; optional
    "time": [ /* TimeObjectSchema entries */ ],
    "lang": "en",            // optional BCP47

    // Workflow-stamped fields. Set by the server when the doc is
    // bound to a workflow; absent on pre-workflow docs.
    "status": "draft",
    "version": 1,
    "workflowId": "wf_...",
    "workflowVersion": 1,
    "publishedVersion": 3,   // present iff the doc has ever published
    "aiPending": { /* present while an AI transition is in flight */ }
  },
  "content": { /* type-specific */ },
  "links": [
    { "type": "core/planning-item", "rel": "planning", "uri": "doc:..." }
  ],
  "history": [ /* append-only history entries; see Workflows */ ]
}

Reserved statuses

Four status names carry platform-wide semantics; tenant-defined statuses are otherwise opaque codes.

Status Meaning
draft Entry status. Always present in every workflow.
usable Read-only, snapshotted. Integrator signal: "publish this".
withheld Read-only, snapshotted. Embargoed / scheduled release.
cancelled Read-only, soft delete. Integrator signal: "unpublish".

Transitions into usable and withheld produce a signed snapshot; the signature lands on the history entry's snapshot field. See Receiving webhooks for the wire format.

core/article

The publishable story - what ends up on a site, in a feed, or in a print system.

{
  "id": "art_...",
  "type": "core/article",
  "title": "rich text",                // headline
  "meta": {
    "priority": 3,
    "time": [
      // Must include `target` (intended publish moment) or `slot`.
      { "type": "target", "time": "2026-06-15T09:00:00Z", "timeZone": "Europe/Stockholm" }
    ],
    "status": "usable",
    "publishedVersion": 3
  },
  "content": {
    "text": "rich text"                 // article body
  },
  "links": [
    // Both links are required.
    { "type": "core/planning-item",       "rel": "planning",   "uri": "doc:pln_..." },
    { "type": "core/planning-assignment", "rel": "assignment", "uri": "doc:asg_..." }
  ]
}

Notable

  • meta.time must contain either a target or a slot entry; validation rejects an article without one.
  • links must include exactly one core/planning-item link and one core/planning-assignment link. Articles are not free-floating.
  • The body lives at content.text as rich text. In webhook snapshot bytes it serialises to a JSON node tree — see Receiving webhooks for the rendering recipe.

When you'll see this

  • Arrives in document.usable webhook payloads as the signed document.snapshotBytes whenever an article transitions to usable (or withheld). See Receiving webhooks.

core/planning-item

The editorial plan a story is produced against. Holds the slugline, description, and the set of assignments that flesh it out.

{
  "id": "pln_...",
  "type": "core/planning-item",
  "title": "rich text",
  "meta": {
    "priority": 3,                      // required, 1-6
    "time": [
      // Both startdate and enddate required. Date-only strings.
      { "type": "startdate", "time": "2026-06-15", "timeZone": "Europe/Stockholm" },
      { "type": "enddate",   "time": "2026-06-16", "timeZone": "Europe/Stockholm" }
    ],
    "status": "usable"
  },
  "content": {
    "slugline": "string",
    "description": "rich text",
    "assignments": [ /* core/planning-assignment, inline */ ]
  },
  "links": []
}

Notable

  • meta.time must contain both a startdate and an enddate entry, as date-only strings (not full timestamps).
  • meta.priority is required and must be 1-6.
  • Assignments live inline under content.assignments as full sub-documents, not as link references.

When you'll see this

  • Arrives in document.usable webhooks the same way articles do, and is referenced by every article via the required core/planning-item link.

core/planning-assignment

A sub-document inside a planning item's content.assignments. Ties the plan to a specific media output and the window it should publish in.

{
  "id": "asg_...",
  "type": "core/planning-assignment",
  "title": "rich text",
  "meta": {
    "type": "text",                     // text | flash | image | graphic | video | audio
    "priority": 3,
    "time": [
      // Optional planning hint. Once the article exists, the
      // article's own meta.time[target] is authoritative.
      { "type": "target", "time": "2026-06-15T09:00:00Z", "timeZone": "Europe/Stockholm" }
    ]
  },
  "content": {
    "slugline": "string",
    "description": "rich text"
  },
  "links": []
}

Notable

  • meta.type is fixed to one of text, flash, image, graphic, video, audio.
  • Assignments are never stored as top-level documents - they live inside a parent core/planning-item. They keep their own id so articles can link back to a specific assignment.
  • meta.time on an assignment is a planning hint; the produced article's meta.time[target] overrides it.

When you'll see this

  • Inline inside any core/planning-item payload, and referenced by the required core/planning-assignment link on every article.

core/image

A tenant-stored image asset. The bytes live in object storage at images/<id>/<variant>; the document carries metadata, rights, provenance, and the signing record. See Media for the upload model, format support, and the rights / provenance contract.

{
  "id": "img_...",
  "type": "core/image",
  "title": "Spring festival - opening parade",
  "meta": {
    "priority": 3,
    "status": "draft",                         // workflow status
    "version": 1,
    "workflowId": "wf_...",
    "workflowVersion": 1,

    // Identity. Server-stamped on finalize.
    "contentHash": "9f86d081884c7d659...",      // SHA-256, lowercase hex
    "originalFormat": "image/jpeg",
    "originalSize": 4823651,                    // bytes
    "dimensions": { "width": 4032, "height": 3024 },

    // Authorship and rights.
    "copyright": "(C) 2026 Jane Doe / Reuters",
    "photographer": "Jane Doe",
    "credit": "Reuters",
    "jobId": "REU-2026-04127",
    "restrictions": true,                       // server-computed
    "instructions": "Free for editorial use during Q2 2026.",
    "usageTerms": "Editorial use only. No derivatives.",

    // Capture (optional - present when EXIF carried them).
    "capturedAt": "2026-04-15T11:23:54Z",
    "capturedAtTimezone": "Europe/Stockholm",
    "geo": { "lat": 59.3293, "lon": 18.0686 },
    "camera": { "make": "Canon", "model": "EOS R5", "lens": "RF24-70mm" },

    // Provenance.
    "c2paManifest": {
      "present": true,
      "issuer": "Adobe",
      "claimGenerator": "Adobe Photoshop 2026",
      "validatedAt": "2026-04-15T11:30:00Z"
    },
    "aiGenerated": false,                       // tri-state: true / false / absent
    "signature": {
      "alg": "ed25519",
      "keyId": "key_...",
      "sig": "base64..."
    },

    // Processing.
    "processingStatus": "ready",                // pending | ready | failed
    "variants": ["original", "thumb", "preview", "delivery"]
  },
  "content": {
    "altText": "Crowd lines the parade route under a cloudy sky.",
    "caption": "rich text",                     // shown beside the image
    "rights": "rich text"                       // longer-form usage notes
  },
  "links": [
    { "type": "core/planning-item",       "rel": "planning",   "uri": "doc:plan_..." },
    { "type": "core/planning-assignment", "rel": "assignment", "uri": "doc:asg_..." }
  ]
}

Notable

  • meta.restrictions is server-computed: true when instructions is set, usageTerms is set, or aiGenerated is true. Surfaces a warning marker on every image render; integrators should mirror that treatment downstream.
  • meta.aiGenerated is tri-state. Absent means "unknown", not "false" - don't conflate the two when filtering.
  • meta.signature is an Ed25519 signature over the domain- prefixed original bytes ("void-image-v1\0" + original). It is in addition to any C2PA chain. See Signing.
  • meta.processingStatus: 'ready' requires contentHash, originalFormat, originalSize, dimensions, and at least a delivery variant - the platform refuses to flip an image to ready without them.

When you'll see this

  • Returned alongside the bytes URL by the media archive endpoints, and embedded in any core/article whose body references an image. Webhook payloads carry the document on its workflow transitions like any other type.

core/newswire

Raw inbound material: wires, press releases, media submissions. Stored verbatim before any editorial work happens against it.

{
  "id": "nws_...",
  "type": "core/newswire",
  "title": "rich text",
  "meta": {
    "priority": 3,
    "origin": "wire",                   // wire | pressrelease | media
    "source": "string",                 // upstream provider id, free text
    "status": "draft"
  },
  "content": {
    "text": "rich text"                 // wire body
  },
  "links": []
}

Notable

  • meta.origin is one of wire, pressrelease, media.
  • meta.source is a free-text upstream identifier (e.g. the wire agency's slug); carried through but not validated against a list.
  • Newswires typically don't change after ingestion - editorial work happens by spawning a core/article that links back.

When you'll see this

  • Push it in via the newswire ingestion endpoint. Shows up in webhook payloads if you subscribe a workflow event to this type; most tenants subscribe articles, not newswires.

core/workflow

The workflow definition itself - the statuses a document type can sit at and the transitions between them. Workflows are documents, so they version and ship through the same pipeline as the content they govern.

See Workflows for the conceptual model and the default-binding rules.

{
  "id": "wf_...",
  "type": "core/workflow",
  "title": "rich text",
  "meta": {
    "priority": 3,
    "appliesToType": "core/article",    // the doc type this governs
    "isDefault": true,                  // at most one default per (tenant, appliesToType)
    "status": "usable"                  // only `usable` workflows are bindable
  },
  "content": {
    "statuses": [
      {
        "name": "draft",                // kebab-case ASCII machine identifier
        "title": "Draft",               // human label
        "description": "Work in progress.",
        "icon": "circle-dashed",
        "color": "neutral"
      }
    ],
    "transitions": [
      {
        "from": "draft",
        "to": "usable",
        "title": "Publish",             // action verb in the status menu
        "requiredRoles": ["editor"],
        "bumpsVersion": false,
        "ai": { /* optional - see core/ai-operation */ },
        "confirm": { /* optional confirmation dialog */ }
      }
    ]
  },
  "links": []
}

Notable

  • A status named draft must exist; it's the entry point for every document on the workflow.
  • Every transition's from, to, and any ai.onSuccess / ai.onFailure must reference a declared status.
  • Transitions leaving a read-only status (usable, withheld, cancelled) to anything other than cancelled must set bumpsVersion: true - re-opening a published doc creates a new editable iteration.
  • Workflows progress through their own hardcoded lifecycle (draft -> usable -> cancelled). Only usable workflows are bindable. The isDefault uniqueness across (tenant, appliesToType) is enforced server-side, not by the schema.

When you'll see this

  • Returned by the workflow management endpoints, and identified on every governed document by meta.workflowId and meta.workflowVersion so consumers can reproduce the rules a transition was decided under.

core/ai-operation

A tenant-defined AI recipe: instructions, static context, the JSON Schema the model's output must satisfy, and how that output writes back into document space. Bound to a workflow transition via transition.ai.operationRef.

See AI integrations for the lifecycle.

{
  "id": "aiop_...",
  "type": "core/ai-operation",
  "title": "rich text",
  "meta": {
    "priority": 3,
    "appliesToType": "core/article",    // primary source doc type
    "connectorId": "conn_...",          // FK into ai_connectors
    "exportName": "translate-to-swedish", // stable ref workflows bind to
    "status": "usable"                  // only `usable` ops fire
  },
  "content": {
    "instructions": "rich text",        // system prompt, free-form prose
    "documentProjection": {             // what slice of the source to send
      "mode": "fields",                 // or "full"
      "paths": ["title", "content.text"]
    },
    "staticContext": [                  // named JSON blobs in the prompt
      { "name": "glossary", "data": "rich text" }
    ],
    "inputs": [                         // optional user-supplied params
      {
        "kind": "enum",                 // or "string"
        "name": "targetLanguage",
        "label": "Target language",
        "required": true,
        "options": [{ "value": "sv", "label": "Swedish" }]
      }
    ],
    "outputSchema": { "schema": "rich text" }, // JSON Schema as text
    "outputTargets": [                  // where output JSON lands
      { "kind": "path",           "from": "title",    "to": "title" },
      { "kind": "textPatch",      "from": "patches",  "targetField": "content.text" },
      { "kind": "textbitReplace", "from": "body",     "targetField": "content.text" },
      { "kind": "createDocument", "from": "extra",    "documentType": "core/article" },
      { "kind": "inputPath",      "from": "targetLanguage", "to": "meta.lang" }
    ],
    "sampling": { "model": "claude-opus-4-7", "temperature": 0.2 },
    "safety":   { "maxInvocationsPerHour": 60, "maxAutoRetries": 2 }
  },
  "links": []
}

Notable

  • meta.exportName is the stable, tenant-scoped name workflows bind to via transition.ai.operationRef. Required (kebab-case) before promotion to usable; tolerated empty during draft edits.
  • Static-context entry names and input names must be unique within the operation - both render as named blocks in the prompt, so a collision would silently shadow one.
  • Output targets must write to distinct destinations: no two path / inputPath may share a to; no two textPatch / textbitReplace may share a targetField. Multiple createDocument targets are fine.
  • An empty outputTargets is valid - the operation runs as advisory and the result lives only in the invocation log.

When you'll see this

  • Authored in the AI operations editor; otherwise an internal asset. Fires when a document arrives at a workflow status whose incoming transition carries an ai.operationRef matching this operation's meta.exportName.

Related pages

  • Workflows — how a workflow resolves a transition, including AI-bound ones.
  • AI integrations — the AI operation lifecycle: dispatch, retries, output write-back, invocation log.
  • Receiving webhooks — webhook payload shape, signature verification, and how document.snapshotBytes relates to the schemas above.