Techlog Event Model
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
| Field | Rule |
|---|---|
schema_version | The constant "0.1-draft". |
id | evt_ followed by 12 characters from [0-9A-Z]. The recorder assigns it if it is absent on submission. |
idempotency_key | A 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. |
type | One of the 13 event types below. |
time | When the event happened, as an RFC 3339 UTC timestamp. |
actor | kind (human, ai_agent, or system), id, and an optional on_behalf_of. |
subject | system, with optional component and environment. |
status | One of the statuses below. |
provenance | recorded_by and method, with an optional confidence. |
Optional core fields
| Field | Meaning |
|---|---|
summary | A short human-readable description, at most 280 characters. |
sources | Where the event came from: a kind and a ref, such as git.pr and promo-api#412. |
evidence | References to supporting artifacts: kind, ref, and an optional sha256 digest. |
links | Typed relationships to other events: rel, target, and basis. |
extensions | Namespaced additions that are not part of the core. |
Event types
| Type | Meaning |
|---|---|
problem.reported | A need, defect, or request was raised. |
investigation.completed | Someone or something examined a problem or a failure and recorded the findings. |
proposal.recorded | A solution was proposed. |
decision.made | A decision was taken, with its approver. |
change.implemented | A change was built and merged. |
verification.completed | Tests or checks ran against a change or a recovery. |
release.deployed | A version reached an environment or a share of traffic. |
incident.opened | Something broke in production. |
mitigation.applied | A temporary measure reduced the impact. |
rollback.executed | A release was reverted. |
recovery.verified | Health was checked and confirmed after a mitigation or rollback. |
postmortem.published | A review of an incident was written up. |
action.tracked | A 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.
Links
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.
| Topic | Behavior |
|---|---|
| Identifier | The 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. |
| Defaults | A 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 duplicates | A 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 basis | A link without a basis is stored as confirmed, except caused_by, which must state its basis. |
| Link targets | Every link target must be an event that already exists, and an event cannot link to itself. |
| Immutability | There 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. |
| Time | Times may carry any offset. The server orders and filters by the instant, not by the text. |
| Errors | A 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" }
}Full: sources, links, extensions
{
"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"
]
}
}
}Incident with a hypothesis link
{
"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"
]
}
}
}
}
}