Techlog Event Model

Enforced by the serverDraft 0.1

Design stance

The model has a small required core and optional extensions. We deliberately do not define a large schema before the use cases are clear. Events are immutable: a later event changes the state of the story, and nothing edits an earlier one.

Required fields

FieldRule
schema_versionThe constant "0.1-draft".
idevt_ followed by 12 characters from [0-9A-Z]. The recorder assigns it if it is absent on submission.
idempotency_keyA string of 1 to 128 characters chosen by the client, unique per actor.id. The server rejects a second event from the same actor with the same key. It is compared exactly: no trimming, no case folding.
typeOne of the 13 event types below.
timeWhen the event happened, as an RFC 3339 UTC timestamp.
actorkind (human, ai_agent, or system), id, and an optional on_behalf_of.
subjectsystem, with optional component and environment.
statusOne of the statuses below.
provenancerecorded_by and method, with an optional confidence.

Optional core fields

FieldMeaning
summaryA short human-readable description, at most 280 characters.
sourcesWhere the event came from: a kind and a ref, such as git.pr and promo-api#412.
evidenceReferences to supporting artifacts: kind, ref, and an optional sha256 digest.
linksTyped relationships to other events: rel, target, and basis.
extensionsNamespaced additions that are not part of the core.

Event types

TypeMeaning
problem.reportedA need, defect, or request was raised.
investigation.completedSomeone or something examined a problem or a failure and recorded the findings.
proposal.recordedA solution was proposed.
decision.madeA decision was taken, with its approver.
change.implementedA change was built and merged.
verification.completedTests or checks ran against a change or a recovery.
release.deployedA version reached an environment or a share of traffic.
incident.openedSomething broke in production.
mitigation.appliedA temporary measure reduced the impact.
rollback.executedA release was reverted.
recovery.verifiedHealth was checked and confirmed after a mitigation or rollback.
postmortem.publishedA review of an incident was written up.
action.trackedA follow-up action was agreed or completed.

Statuses

There are nine: proposed, recorded, in_progress, verified, failed, open, mitigated, resolved, and superseded. A status describes the thing the event records at the moment it was recorded. When the situation changes, a new event is recorded; the old one keeps its status.

The rel values are motivated_by, investigates, decides, implements, verifies, releases, observed_after, mitigates, reverts, resolves, follows_up, caused_by, and relates_to. Each link has a basis: confirmed or hypothesis.

Confirmed or hypothesis? A confirmed link says "this is established", such as a change that implements a proposal. A hypothesis says "this is suspected". Only causal claims (caused_by) are expected to be hypotheses until another event corroborates them. In the Spring Spin example, the canary incident is first linked to the change as a hypothesis, and a later investigation event confirms it.

Extensions

Extension keys are namespaced: lowercase, dot-separated, in a reverse-DNS style such as org.example.topic. Consumers must ignore extensions they do not know. An extension must not change the meaning of a core field.

What the server adds to the schema

The Techlog server validates every submitted event against the JSON Schema below, whether it arrives over the HTTP API or over MCP. It also applies a few rules and defaults that a schema cannot express.

TopicBehavior
IdentifierThe server assigns id: evt_, then eight base-36 characters of the creation time in milliseconds, then four random ones, so ids sort by creation time. An id in a submission is ignored.
DefaultsA missing schema_version becomes "0.1-draft"; a missing time becomes the time of recording; a missing provenance.method becomes human_entered over HTTP and agent_submitted over MCP; a missing provenance.recorded_by becomes the actor's id.
Retries and duplicatesA submission whose idempotency_key the actor already used is rejected and nothing is stored. Over HTTP the response is 409 with code duplicate_event and error.existing_id; over MCP it is an error result naming the existing event. The server does not replay the original event: fetch it by id. Validation errors are reported before duplicates.
Link basisA link without a basis is stored as confirmed, except caused_by, which must state its basis.
Link targetsEvery link target must be an event that already exists, and an event cannot link to itself.
ImmutabilityThere is no update and no delete. A link added later is stored beside the event, not inside it. Adding the same link twice does nothing; adding it with the other basis stores a second link, so a hypothesis can be corroborated by a confirmed link without editing anything.
TimeTimes may carry any offset. The server orders and filters by the instant, not by the text.
ErrorsA rejected event returns every problem at once, each with the JSON Pointer path of the field and a message, for example /links/0/target.

Examples

Minimal: required fields only

{
  "schema_version": "0.1-draft",
  "id": "evt_01J9K2A0Q001",
  "idempotency_key": "promo-api-report-2026-03-12-m.okafor",
  "type": "problem.reported",
  "time": "2026-03-12T08:14:00Z",
  "actor": { "kind": "human", "id": "user:m.okafor" },
  "subject": { "system": "promo-api" },
  "status": "recorded",
  "provenance": { "recorded_by": "user:m.okafor", "method": "human_entered" }
}
{
  "schema_version": "0.1-draft",
  "id": "evt_01J9K2A0P006",
  "idempotency_key": "seed-spring-spin-006",
  "type": "change.implemented",
  "time": "2026-03-11T10:12:00Z",
  "actor": {
    "kind": "ai_agent",
    "id": "agent:code-assistant",
    "on_behalf_of": "user:a.novak"
  },
  "subject": {
    "system": "promo-api",
    "component": "spin-service"
  },
  "status": "recorded",
  "summary": "Spin endpoint with server-side draw, 20% cap, and one spin per new account; reviewed and merged",
  "sources": [
    {
      "kind": "git.pr",
      "ref": "promo-api#412"
    },
    {
      "kind": "git.commit",
      "ref": "promo-api@9c3e7ab"
    }
  ],
  "links": [
    {
      "rel": "implements",
      "target": "evt_01J9K2A0P004",
      "basis": "confirmed"
    },
    {
      "rel": "relates_to",
      "target": "evt_01J9K2A0P005",
      "basis": "confirmed"
    }
  ],
  "provenance": {
    "recorded_by": "collector:git",
    "method": "collected"
  },
  "extensions": {
    "org.techlog.code": {
      "files_changed": 5,
      "lines_added": 164,
      "lines_removed": 22,
      "reviewers": [
        "user:s.lindqvist"
      ]
    }
  }
}
{
  "schema_version": "0.1-draft",
  "id": "evt_01J9K2A0P011",
  "idempotency_key": "seed-spring-spin-011",
  "type": "incident.opened",
  "time": "2026-03-12T13:38:00Z",
  "actor": {
    "kind": "system",
    "id": "alert:error-rate-monitor"
  },
  "subject": {
    "system": "promo-api",
    "component": "spin-service",
    "environment": "production"
  },
  "status": "open",
  "summary": "5xx ratio on the spin endpoint reached 6.2% on the canary, above the 1.0% rollback threshold for 3 minutes",
  "evidence": [
    {
      "kind": "alert",
      "ref": "alert://promo-api/high-error-rate/a-5521"
    },
    {
      "kind": "metric.query",
      "ref": "metrics://promo-api/http_5xx_ratio?canary=true"
    }
  ],
  "links": [
    {
      "rel": "observed_after",
      "target": "evt_01J9K2A0P010",
      "basis": "confirmed"
    },
    {
      "rel": "caused_by",
      "target": "evt_01J9K2A0P006",
      "basis": "hypothesis"
    }
  ],
  "provenance": {
    "recorded_by": "collector:alerting",
    "method": "collected"
  },
  "extensions": {
    "org.techlog.hypothesis": {
      "statement": "the new spin endpoint may fail under production traffic patterns",
      "status": "unconfirmed",
      "next_step": "Replay canary traffic against the spin service and inspect the failing requests"
    }
  }
}

JSON Schema (draft)

This is the schema the server enforces for the core. It is a draft in this repository, not a published schema. A test keeps the server's embedded copy identical to this file.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://techlog.example.invalid/schema/0.1-draft/event.json",
  "title": "Techlog Event (proposed, draft 0.1)",
  "type": "object",
  "required": [
    "schema_version",
    "id",
    "idempotency_key",
    "type",
    "time",
    "actor",
    "subject",
    "status",
    "provenance"
  ],
  "additionalProperties": false,
  "properties": {
    "schema_version": {
      "const": "0.1-draft"
    },
    "id": {
      "type": "string",
      "pattern": "^evt_[0-9A-Z]{12}$"
    },
    "idempotency_key": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "description": "Chosen by the client. Unique per actor.id: the server rejects a second event from the same actor with the same key. Compared byte for byte."
    },
    "type": {
      "enum": [
        "problem.reported",
        "investigation.completed",
        "proposal.recorded",
        "decision.made",
        "change.implemented",
        "verification.completed",
        "release.deployed",
        "incident.opened",
        "mitigation.applied",
        "rollback.executed",
        "recovery.verified",
        "postmortem.published",
        "action.tracked"
      ]
    },
    "time": {
      "type": "string",
      "format": "date-time"
    },
    "actor": {
      "$ref": "#/$defs/actor"
    },
    "subject": {
      "$ref": "#/$defs/subject"
    },
    "status": {
      "enum": [
        "proposed",
        "recorded",
        "in_progress",
        "verified",
        "failed",
        "open",
        "mitigated",
        "resolved",
        "superseded"
      ]
    },
    "summary": {
      "type": "string",
      "maxLength": 280
    },
    "sources": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/source"
      }
    },
    "evidence": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/evidence"
      }
    },
    "links": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/link"
      }
    },
    "provenance": {
      "$ref": "#/$defs/provenance"
    },
    "extensions": {
      "type": "object",
      "propertyNames": {
        "pattern": "^[a-z0-9]+(\\.[a-z0-9-]+)+$"
      }
    }
  },
  "$defs": {
    "actor": {
      "type": "object",
      "required": [
        "kind",
        "id"
      ],
      "additionalProperties": false,
      "properties": {
        "kind": {
          "enum": [
            "human",
            "ai_agent",
            "system"
          ]
        },
        "id": {
          "type": "string"
        },
        "on_behalf_of": {
          "type": "string"
        }
      }
    },
    "subject": {
      "type": "object",
      "required": [
        "system"
      ],
      "additionalProperties": false,
      "properties": {
        "system": {
          "type": "string"
        },
        "component": {
          "type": "string"
        },
        "environment": {
          "type": "string"
        }
      }
    },
    "source": {
      "type": "object",
      "required": [
        "kind",
        "ref"
      ],
      "additionalProperties": false,
      "properties": {
        "kind": {
          "type": "string"
        },
        "ref": {
          "type": "string"
        }
      }
    },
    "evidence": {
      "type": "object",
      "required": [
        "kind",
        "ref"
      ],
      "additionalProperties": false,
      "properties": {
        "kind": {
          "type": "string"
        },
        "ref": {
          "type": "string"
        },
        "digest": {
          "type": "string",
          "pattern": "^sha256:[0-9a-f]{64}$"
        }
      }
    },
    "link": {
      "type": "object",
      "required": [
        "rel",
        "target",
        "basis"
      ],
      "additionalProperties": false,
      "properties": {
        "rel": {
          "enum": [
            "motivated_by",
            "investigates",
            "decides",
            "implements",
            "verifies",
            "releases",
            "observed_after",
            "mitigates",
            "reverts",
            "resolves",
            "follows_up",
            "caused_by",
            "relates_to"
          ]
        },
        "target": {
          "type": "string",
          "pattern": "^evt_[0-9A-Z]{12}$"
        },
        "basis": {
          "enum": [
            "confirmed",
            "hypothesis"
          ]
        }
      }
    },
    "provenance": {
      "type": "object",
      "required": [
        "recorded_by",
        "method"
      ],
      "additionalProperties": false,
      "properties": {
        "recorded_by": {
          "type": "string"
        },
        "method": {
          "enum": [
            "collected",
            "agent_submitted",
            "human_entered",
            "derived"
          ]
        },
        "confidence": {
          "enum": [
            "asserted",
            "corroborated"
          ]
        }
      }
    }
  }
}