Getting Started
These samples run. They call the Techlog server in this repository, started on your machine at http://localhost:8080. The server implements no authentication and no event signatures, so the calls send no token. The proposed signing and access model is described under security and trust.
Start the server
From the root of this repository, load the example story and start the server. You need Go 1.27 or later.
make seed # load the Spring Spin example story into share/techlog.db
make run # serve the UI, the API and MCP at http://localhost:8080
make skill # optional: install the Claude skill into ~/.claude/skillsThe UI is now at http://localhost:8080/. Run the server explains the options.
What you will do
- Record a proposal that is linked to the problem that motivated it.
- Record the verification result, with the test report as evidence.
- Read the recent history of the system.
The samples use the Spring Spin story: a service called promo-api and a discount game for new clients. make seed loads that story, so the problem report the proposal links to already exists.
A first event
This is the smallest valid event. Every field here is required. The idempotency_key is yours to choose: reuse it when you retry, and the server rejects the repeat. The event model explains each one. When you submit an event, leave out the id: the server assigns it, and ignores one you send. time and schema_version may also be left out; the server fills them in.
{
"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" }
}Choose your language
Python 3.9+, standard library only.
"""Record events in a local Techlog server. Start it first: make seed && make run."""
import json
import os
import urllib.request
BASE = os.environ.get("TECHLOG_URL", "http://localhost:8080")
def call(method, path, body=None):
request = urllib.request.Request(
BASE + path,
method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request) as response:
return json.load(response)
# 1. Record a proposal, linked to the original problem report.
# The server assigns the id and, because "time" is omitted, the time. The
# idempotency_key makes a retry safe: reusing it is rejected with 409.
proposal = call("POST", "/api/v1/events", {
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-proposal",
"type": "proposal.recorded",
"actor": {"kind": "ai_agent", "id": "agent:code-assistant", "on_behalf_of": "user:a.novak"},
"subject": {"system": "promo-api", "component": "spin-service"},
"status": "proposed",
"summary": "Spin-to-win: one spin per new account, discount capped at 20%",
"links": [{"rel": "motivated_by", "target": "evt_01J9K2A0P002", "basis": "confirmed"}],
"provenance": {"recorded_by": "agent:code-assistant", "method": "agent_submitted"},
})["event"]
print("recorded", proposal["id"])
# 2. Record the verification result. The test report is evidence, kept as a
# reference inside the event.
call("POST", "/api/v1/events", {
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-verification-ci-90412",
"type": "verification.completed",
"actor": {"kind": "system", "id": "ci:pipeline"},
"subject": {"system": "promo-api", "component": "spin-service"},
"status": "verified",
"summary": "Unit, integration, and abuse tests passed",
"evidence": [{
"kind": "test.report",
"ref": "ci://run/90412",
"digest": "sha256:7b1e4d9a2c6f08e35a9d1b7c4e2f6a8091d3c5b7e9f0a2c4d6e8b0a1c3e5f7d9",
}],
"links": [{"rel": "verifies", "target": "evt_01J9K2A0P006", "basis": "confirmed"}],
"provenance": {"recorded_by": "collector:ci", "method": "collected"},
})
# 3. Read the latest events of the system, oldest first.
history = call("GET", "/api/v1/systems/promo-api/history?limit=5")
for e in history["events"]:
print(e["time"], e["type"], e.get("summary", ""))Node 18+ with the built-in fetch, run as an ES module.
// Record events in a local Techlog server. Start it first: make seed && make run.
const BASE = process.env.TECHLOG_URL ?? "http://localhost:8080";
async function call<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(BASE + path, {
method,
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${await res.text()}`);
return (await res.json()) as T;
}
// 1. Record a proposal, linked to the original problem report.
// The server assigns the id and, because "time" is omitted, the time. The
// idempotency_key makes a retry safe: reusing it is rejected with 409.
const { event: proposal } = await call<{ event: { id: string } }>("POST", "/api/v1/events", {
schema_version: "0.1-draft",
idempotency_key: "promo-api-spin-proposal",
type: "proposal.recorded",
actor: { kind: "ai_agent", id: "agent:code-assistant", on_behalf_of: "user:a.novak" },
subject: { system: "promo-api", component: "spin-service" },
status: "proposed",
summary: "Spin-to-win: one spin per new account, discount capped at 20%",
links: [{ rel: "motivated_by", target: "evt_01J9K2A0P002", basis: "confirmed" }],
provenance: { recorded_by: "agent:code-assistant", method: "agent_submitted" },
});
console.log("recorded", proposal.id);
// 2. Record the verification result. The test report is evidence, kept as a
// reference inside the event.
await call("POST", "/api/v1/events", {
schema_version: "0.1-draft",
idempotency_key: "promo-api-spin-verification-ci-90412",
type: "verification.completed",
actor: { kind: "system", id: "ci:pipeline" },
subject: { system: "promo-api", component: "spin-service" },
status: "verified",
summary: "Unit, integration, and abuse tests passed",
evidence: [{
kind: "test.report",
ref: "ci://run/90412",
digest: "sha256:7b1e4d9a2c6f08e35a9d1b7c4e2f6a8091d3c5b7e9f0a2c4d6e8b0a1c3e5f7d9",
}],
links: [{ rel: "verifies", target: "evt_01J9K2A0P006", basis: "confirmed" }],
provenance: { recorded_by: "collector:ci", method: "collected" },
});
// 3. Read the latest events of the system, oldest first.
const history = await call<{ events: { time: string; type: string; summary?: string }[] }>(
"GET", "/api/v1/systems/promo-api/history?limit=5");
for (const e of history.events) console.log(e.time, e.type, e.summary ?? "");Go 1.21+, standard library only.
// Record events in a local Techlog server. Start it first: make seed && make run.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
func env(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
var base = env("TECHLOG_URL", "http://localhost:8080")
func call(method, path string, body any, out any) error {
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return err
}
}
req, err := http.NewRequest(method, base+path, &buf)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
msg, _ := io.ReadAll(res.Body)
return fmt.Errorf("%s %s -> %d: %s", method, path, res.StatusCode, msg)
}
if out == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(out)
}
func main() {
// 1. Record a proposal, linked to the original problem report.
// The server assigns the id and, because "time" is omitted, the time. The
// idempotency_key makes a retry safe: reusing it is rejected with 409.
var proposal struct {
Event struct {
ID string `json:"id"`
} `json:"event"`
}
err := call("POST", "/api/v1/events", map[string]any{
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-proposal",
"type": "proposal.recorded",
"actor": map[string]any{"kind": "ai_agent", "id": "agent:code-assistant", "on_behalf_of": "user:a.novak"},
"subject": map[string]any{"system": "promo-api", "component": "spin-service"},
"status": "proposed",
"summary": "Spin-to-win: one spin per new account, discount capped at 20%",
"links": []map[string]any{{"rel": "motivated_by", "target": "evt_01J9K2A0P002", "basis": "confirmed"}},
"provenance": map[string]any{"recorded_by": "agent:code-assistant", "method": "agent_submitted"},
}, &proposal)
if err != nil {
log.Fatal(err)
}
fmt.Println("recorded", proposal.Event.ID)
// 2. Record the verification result. The test report is evidence, kept as
// a reference inside the event.
err = call("POST", "/api/v1/events", map[string]any{
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-verification-ci-90412",
"type": "verification.completed",
"actor": map[string]any{"kind": "system", "id": "ci:pipeline"},
"subject": map[string]any{"system": "promo-api", "component": "spin-service"},
"status": "verified",
"summary": "Unit, integration, and abuse tests passed",
"evidence": []map[string]any{{
"kind": "test.report",
"ref": "ci://run/90412",
"digest": "sha256:7b1e4d9a2c6f08e35a9d1b7c4e2f6a8091d3c5b7e9f0a2c4d6e8b0a1c3e5f7d9",
}},
"links": []map[string]any{{"rel": "verifies", "target": "evt_01J9K2A0P006", "basis": "confirmed"}},
"provenance": map[string]any{"recorded_by": "collector:ci", "method": "collected"},
}, nil)
if err != nil {
log.Fatal(err)
}
// 3. Read the latest events of the system, oldest first.
var history struct {
Events []struct {
Time string `json:"time"`
Type string `json:"type"`
Summary string `json:"summary"`
} `json:"events"`
}
if err := call("GET", "/api/v1/systems/promo-api/history?limit=5", nil, &history); err != nil {
log.Fatal(err)
}
for _, e := range history.Events {
fmt.Println(e.Time, e.Type, e.Summary)
}
}Java 17+, JDK only. Prints the raw JSON responses.
// Record events in a local Techlog server. Start it first: make seed && make run.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Quickstart {
static final String BASE = System.getenv().getOrDefault("TECHLOG_URL", "http://localhost:8080");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String json) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Content-Type", "application/json");
b.method(method, json == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(json));
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 300) throw new RuntimeException(method + " " + path + " -> " + res.statusCode() + ": " + res.body());
return res.body();
}
public static void main(String[] args) throws Exception {
// 1. Record a proposal, linked to the original problem report.
// The server assigns the id and, because "time" is omitted, the time. The
// idempotency_key makes a retry safe: reusing it is rejected with 409.
String created = call("POST", "/api/v1/events", """
{
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-proposal",
"type": "proposal.recorded",
"actor": { "kind": "ai_agent", "id": "agent:code-assistant", "on_behalf_of": "user:a.novak" },
"subject": { "system": "promo-api", "component": "spin-service" },
"status": "proposed",
"summary": "Spin-to-win: one spin per new account, discount capped at 20%",
"links": [{ "rel": "motivated_by", "target": "evt_01J9K2A0P002", "basis": "confirmed" }],
"provenance": { "recorded_by": "agent:code-assistant", "method": "agent_submitted" }
}
""");
Matcher m = Pattern.compile("\"id\"\\s*:\\s*\"(evt_[0-9A-Z]+)\"").matcher(created);
if (!m.find()) throw new RuntimeException("no id in response: " + created);
System.out.println("recorded " + m.group(1));
// 2. Record the verification result. The test report is evidence, kept
// as a reference inside the event.
call("POST", "/api/v1/events", """
{
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-verification-ci-90412",
"type": "verification.completed",
"actor": { "kind": "system", "id": "ci:pipeline" },
"subject": { "system": "promo-api", "component": "spin-service" },
"status": "verified",
"summary": "Unit, integration, and abuse tests passed",
"evidence": [{
"kind": "test.report",
"ref": "ci://run/90412",
"digest": "sha256:7b1e4d9a2c6f08e35a9d1b7c4e2f6a8091d3c5b7e9f0a2c4d6e8b0a1c3e5f7d9"
}],
"links": [{ "rel": "verifies", "target": "evt_01J9K2A0P006", "basis": "confirmed" }],
"provenance": { "recorded_by": "collector:ci", "method": "collected" }
}
""");
// 3. Read the latest events of the system, oldest first.
System.out.println(call("GET", "/api/v1/systems/promo-api/history?limit=5", null));
}
}curl and bash.
#!/usr/bin/env bash
# Record events in a local Techlog server. Start it first: make seed && make run.
set -euo pipefail
TECHLOG_URL="${TECHLOG_URL:-http://localhost:8080}"
# 1. Record a proposal, linked to the original problem report.
# The server assigns the id and, because "time" is omitted, the time. The
# idempotency_key makes a retry safe: reusing it is rejected with 409.
ID=$(curl -fsS -X POST "$TECHLOG_URL/api/v1/events" -H "Content-Type: application/json" \
-d '{
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-proposal",
"type": "proposal.recorded",
"actor": { "kind": "ai_agent", "id": "agent:code-assistant", "on_behalf_of": "user:a.novak" },
"subject": { "system": "promo-api", "component": "spin-service" },
"status": "proposed",
"summary": "Spin-to-win: one spin per new account, discount capped at 20%",
"links": [{ "rel": "motivated_by", "target": "evt_01J9K2A0P002", "basis": "confirmed" }],
"provenance": { "recorded_by": "agent:code-assistant", "method": "agent_submitted" }
}' | grep -o '"id":"evt_[0-9A-Z]*"' | head -n 1 | cut -d'"' -f4)
echo "recorded $ID"
# 2. Record the verification result. The test report is evidence, kept as a
# reference inside the event.
curl -fsS -X POST "$TECHLOG_URL/api/v1/events" -H "Content-Type: application/json" \
-d '{
"schema_version": "0.1-draft",
"idempotency_key": "promo-api-spin-verification-ci-90412",
"type": "verification.completed",
"actor": { "kind": "system", "id": "ci:pipeline" },
"subject": { "system": "promo-api", "component": "spin-service" },
"status": "verified",
"summary": "Unit, integration, and abuse tests passed",
"evidence": [{
"kind": "test.report",
"ref": "ci://run/90412",
"digest": "sha256:7b1e4d9a2c6f08e35a9d1b7c4e2f6a8091d3c5b7e9f0a2c4d6e8b0a1c3e5f7d9"
}],
"links": [{ "rel": "verifies", "target": "evt_01J9K2A0P006", "basis": "confirmed" }],
"provenance": { "recorded_by": "collector:ci", "method": "collected" }
}' > /dev/null
# 3. Read the latest events of the system, oldest first.
curl -fsS "$TECHLOG_URL/api/v1/systems/promo-api/history?limit=5"What the calls do
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/events | Record an event. The server validates it, assigns the id, and returns the stored event as {"event": {...}} with status 201. |
GET | /api/v1/systems/{system}/history | Read the latest events of one system, oldest first. |
An invalid event gets status 422 and a list of problems, each with the JSON Pointer path of the field to fix. Evidence is recorded inside the event, as references; there is no separate evidence endpoint. The full list of routes is on Run the server.
What runs today and what is proposed
| Runs today (this repository) | Proposed |
|---|---|
| One Go binary with the event API, an MCP server, a storage layer with a SQLite backend, a read-only web UI, and a Claude skill. Events are validated against the draft 0.1 schema. | Collectors for git, CI, deploy, and alerting systems; policy evaluation; event signatures and authentication; further storage backends. |
Run this site locally
The documentation site is a separate, static build in the website/ directory:
npm run build # assemble src/ into site/ (offline; Node permission model, no network)
npm test # run the build, example and tool tests
npm run check # check links and run the content audit on site/
npm run serve # preview at http://localhost:8090 (python3 required)