Tidenda

Concepts

AI integrations

Connectors, operations, adapters, the dispatch lifecycle, replay and skip semantics.

AI integrations

Tidenda lets an organisation wire LLM and JSON-API providers into its editorial workflow. The mechanics live in two document types:

  • Connector (core/ai-connector) — provider credentials and a kind that picks the wire format (OpenAI-compatible, Anthropic, Mistral, or a generic JSON POST). One connector typically powers many operations.
  • Operation (core/ai-operation) — a recipe: what slice of the source document to send, the prompt, optional user inputs, the JSON Schema the response must satisfy, and where each piece of the response should land in document space.

A workflow transition with an ai block wires an operation to an editorial event. When an operator takes the transition, the operation runs; on success its outputs write back into the document and the transition lands at its target status.

This page covers the conceptual model and the operation shape. For the workflow side see Workflows; for the JSON shape see Document formats.

Why the connector / operation split

Rotating an API key is a connector edit. Tweaking a prompt is an operation edit. Keeping them separate means the team who manages provider credentials doesn't have to coordinate with the team who tunes prompts, and a single connector outage is visible in one place even when a dozen operations depend on it.

Operations are tenant-scoped; connectors are tenant-scoped. Neither crosses tenant boundaries.

Operation anatomy

An operation declares everything the platform needs to run an AI call. All fields below live under content unless noted.

Source projection

documentProjection controls what the model sees:

  • { "mode": "full" } sends the entire source document minus its history.
  • { "mode": "fields", "paths": ["title", "content.text"] } sends only the named dot-paths. Use this when the document carries bulky fields the model doesn't need.

A fields projection that selects zero matching paths fails the operation rather than calling the model with an empty body.

Instructions

instructions is the system prompt. Author it as prose in the operation editor. It is rendered into the provider-appropriate slot (system role on OpenAI-compatible providers and Mistral, top-level system field on Anthropic, instructions field on generic-json-post).

Static context

staticContext is a list of named JSON blobs the operator authors. At dispatch, each blob is rendered into the prompt as a JSON object keyed by name. Use this for things like glossaries, category lists, style guides — knowledge the prompt should reference but which would clutter the prose.

Names must be unique within an operation.

Inputs

inputs are values the operator picks at the moment they fire the transition. Two kinds:

  • enum — closed list of options, each with a value, label, and optional extras JSON the prompt can reference.
  • string — free text, with optional placeholder.

Both support required and default. When an operation declares any inputs, the transitions menu opens a picker dialog before the transition fires; the collected values are rendered into the prompt as a userInputs JSON object.

Output schema

outputSchema is a JSON Schema (Draft 2020-12) the model's response must satisfy. The platform compiles the schema on every dispatch (so editing it takes effect on the next call) and validates the response against it after the call returns — regardless of whether the provider claims to enforce strict mode. The operation's schema is the final word.

Output targets

outputTargets describes where pieces of the validated response land. Each target is one of:

  • path — read a value from the AI output, write it at a document path. Suitable for metadata writes (a category id, a language code, a summary string).

    { "kind": "path", "from": "categoryId", "to": "meta.categoryId" }
    
  • inputPath — write a user-supplied input directly onto the document. The canonical case is translation: the operator picks the target language, the model translates the body, inputPath records the language code on the document in the same transition.

    { "kind": "inputPath", "from": "lang", "to": "meta.lang" }
    
  • textPatch — apply a list of { id, text } patches into the rich-text tree at targetField. Preserves the document's structural shape (images, layouts, captions). Inline marks inside patched elements are not preserved.

    { "kind": "textPatch", "from": "patches", "targetField": "content.text" }
    
  • textbitReplace — rebuild the whole rich-text field at targetField from a fresh node array. Use for ground-up rewrites where the prior structure isn't preserved.

    { "kind": "textbitReplace", "from": "body", "targetField": "content.text" }
    
  • createDocument — build a brand-new document of the given type from the response. The source document is unaffected. Use for "generate a companion piece from this article" patterns.

    { "kind": "createDocument", "from": "translated", "documentType": "core/article" }
    

Two targets cannot write to the same destination (to for path / inputPath, targetField for textPatch / textbitReplace). Multiple createDocument targets are fine.

Sampling and safety

sampling sets provider-specific parameters (model, temperature, etc.). safety caps the per-tenant per-hour invocation count and the number of automatic retries on transient failures. Both are tuned per operation; the platform enforces them.

Export name

meta.exportName is the stable, kebab-case name workflows bind to via transition.ai.operationRef. Required (and validated) before the operation can reach usable. Two operations cannot share an export name in the same tenant.

When a workflow looks up its bound operation, it resolves the latest usable operation with the matching export name. Editing an operation does not require re-publishing the workflow — promote the new version and traffic flips on the next call.

Connector kinds

The connector's kind selects an adapter that knows how to talk to the provider. All adapters share the same input contract, so swapping a connector on an operation doesn't require touching the operation.

Kind Targets Structured output via
openai-compatible OpenAI, Azure OpenAI, Ollama, Groq, Together, vLLM response_format: { type: "json_schema", strict: true }
anthropic Anthropic Messages API A forced tool call whose input_schema is the operation's schema
mistral Mistral La Plateforme A forced function call whose parameters is the operation's schema
generic-json-post Anything else (internal AI services, non-LLM APIs, vendor-locked APIs) A flat envelope POSTed to a single URL; response satisfies the schema directly
local-service Built-in local AI service shipped with the platform (translation, summarisation, IPTC categorisation) Same envelope as generic-json-post; identified separately so cost ceilings can treat the calls as zero-spend

For openai-compatible, anthropic, and mistral, the platform's own post-call validation runs regardless of provider-side enforcement.

Built-in local AI service

The platform ships with three local enrichment endpoints (the local-service row above). Models are loaded into a container that runs alongside the rest of the platform; tenants pay no per-call cost and no document text leaves the deployment.

Capability Model Where it runs
Translation between operator-picked language pairs facebook/nllb-200-distilled-600M Bundled local service
Short summarisation for article descriptions csebuetnlp/mT5_multilingual_XLSum Same
Zero-shot IPTC top-level categorisation MoritzLaurer/mDeBERTa-v3-base-mnli-xnli Same

All three are open-weight Hugging Face models and the model identifiers above are the defaults. Each capability has its own operation slot in the operation catalogue (translate-from-wires, summarize-from-wires, categorize-newswire) — an organisation that wants to swap the local model for its own LLM duplicates the slot's operation, picks its own connector, and the platform routes through the override automatically.

Newswire categorisation on arrival

Every incoming core/newswire document is classified against the 17 top-level IPTC MediaTopic categories the moment ingestion finishes. The classification is asynchronous — the wire is visible to the editorial UI immediately at usable, and the category lands on meta.ipctCategory once the classifier returns. A classification failure leaves the wire usable but uncategorised; the operator can still open it, create an article from it, and so on.

The category drives two surfaces in the newswire view:

  • A pill on the wire row showing the topic in short form (Sport, Politics, Business…). Clicking the pill toggles that category in the column filter.
  • A multi-select category filter in the toolbar. Categories combine OR-wise within the filter and AND-wise with the source filter.

Categorisation never propagates into articles created from a wire. Articles carry their own categorisation if and when the editorial flow adds one.

For generic-json-post, the request body is:

{
  "instructions": "...",
  "document": { "...": "..." },
  "staticContext": [{ "name": "...", "data": "..." }],
  "outputSchema": { "name": "...", "schema": { "...": "..." } },
  "sampling": { "model": "...", "...": "..." }
}

The response must satisfy the operation's output schema directly (no envelope to strip).

Binding to a workflow

A workflow transition that fires AI carries an ai block:

{
  "from": "draft",
  "to": "translated",
  "ai": {
    "operationRef": "translate-article",
    "onFailure": "translation-failed"
  }
}
  • operationRef — the operation's meta.exportName. Resolved at dispatch to the latest usable operation with that name.
  • onSuccess is implicit: the transition's to.
  • onFailure is optional. When set, content-shaped failures (validation, projection, response parse) transition the document there instead of parking it. Infrastructure failures (auth, cost limit, connector disabled) never transition — the document stays put with the error visible so a config fix can let the operator retry.

A worked example

An operation translate-article declares one enum input named lang with options for Swedish, Danish, Norwegian. Its output schema describes { title: string, patches: [{ id, text }] }. Its targets are:

[
  { "kind": "path",      "from": "title",   "to": "title" },
  { "kind": "textPatch", "from": "patches", "targetField": "content.text" },
  { "kind": "inputPath", "from": "lang",    "to": "meta.lang" }
]

A workflow transition draft → translated references the operation. When an editor takes the transition the picker opens, the user selects Swedish, the operation runs, and the document arrives at translated with a translated title, patched body, and meta.lang = "sv" — all in a single version bump.

Dispatch lifecycle

  1. The operator takes an AI-bound transition. If inputs are declared, the picker dialog opens first.
  2. The platform stamps meta.aiPending on the document, locks it editing-wise, and displays an "AI working" banner.
  3. A pre-AI snapshot of the document is captured for rollback.
  4. The dispatcher projects the document per documentProjection, parses static context entries, synthesises the userInputs block from the picker values, and calls the connector.
  5. The response is validated against outputSchema.
  6. On success, every target in outputTargets is applied in a single transaction; the document arrives at the transition's target status and meta.aiPending is cleared.
  7. createDocument targets run afterwards as new documents in their own right.
  8. The invocation log records the call (prompt, response, duration, per-target warnings) regardless of outcome.

On permanent failure the document either transitions to onFailure (if set and the failure is content-shaped) or stays at its current status with the error visible. The operator can cancel a pending operation from the banner; Cancel clears aiPending without applying any writes.

Failure categories

Category Class Retried automatically? Notes
Network blip / 5xx transient yes Up to safety.maxAutoRetries.
Provider timeout transient yes Same.
Validation against schema permanent no Transitions to onFailure when set.
Projection error permanent no Same.
Response parse error permanent no Same.
Auth, cost limit permanent no Document stays put with the error visible.
Connector disabled permanent no Same.

A document that stays pending past the stale timeout (default 10 minutes) is swept clean automatically; the invocation log records the timeout and the document returns to its previous status.

Snapshots and rollback

Every AI apply captures a pre-AI snapshot first, identified by the operation id and a timestamp. The snapshot is the rollback point. In the admin interface, the invocation log shows the snapshot reference for each completed operation; rolling back is a manual step (push the snapshot bytes back as the current document state) — there is no one-click revert yet.

For the integrator's view of an AI step from outside the platform, see Receiving webhooks. The webhook fires only on the final transition; the AI step is invisible to a webhook receiver beyond a by.kind = "system" credit on the history entry.

Related pages