SSyncropel Docs

Record Format

The 8-field record — wire format, canonical JSON rules, ID derivation, act types, data types, common body conventions.

The 8 fields

Every record has exactly 8 fields. Seven are hashed into the ID; one (judged_by) is not.

#FieldTypeHashedDescription
1parentsstring[]yesRecord IDs this builds on. Sorted ascending. [] for a root record.
2threadstringyesThread ID: th_ + 64 hex characters.
3actorstringyesActor DID (did:sync:user:alice, did:sync:agent:dev, did:key:…, etc.).
4actstringyesOne of 8 act types (see below).
5bodyobjectyesJSON payload. Kernel-opaque; conventions documented per topic.
6clockintegeryesLogical timestamp. Monotonic per (actor, thread). Non-negative, fits in i64.
7data_typestringyesOne of 6 data types (see below).
8judged_bystring | nullnoPointer to the record whose judgment this reflects. Often null.

ID derivation

id = SHA-256(canonical_json(act, actor, body, clock, data_type, parents, thread))

Seven fields, alphabetical key order, canonical JSON encoding, SHA-256. The result is lowercase hex, 64 characters, no prefix.

The judged_by field is excluded from the hash because the same observation reviewed by two different parties is the same observation. Excluding judged_by keeps the ID stable across reviews while letting trust attribution vary.

Canonical JSON encoding

The hashing function operates on canonical JSON bytes. Every implementation must produce identical bytes for the same semantic input.

Rules:

  1. Keys sorted alphabetically at every depth. {"b": 1, "a": 2} serializes as {"a":2,"b":1}.
  2. No whitespace outside of string values. No newlines, no indentation.
  3. Numbers as the minimal representation. No leading zeros. No trailing zeros in fractions. No exponent unless required by magnitude.
  4. Strings use JSON escaping. Control characters through  are \uXXXX-escaped. " and \ are escaped. UTF-8 bytes pass through otherwise.
  5. Booleans and null as true, false, null.
  6. Arrays preserve element order. This matters for parents — preserve ascending sort.

A reference encoder is in syncropel-algebra::canonical::encode (Rust) and syncropel.canonical.canonical_json (Python SDK). Both produce identical bytes for identical semantic input. See the 7 test vectors for conformance cases.

Thread IDs

thread_id = "th_" + SHA-256(<seed inputs>)

The th_ prefix is non-negotiable — it's how downstream code distinguishes thread IDs from record IDs (bare 64-hex) and DIDs (did:…). Validators reject records whose thread field doesn't start with th_ followed by exactly 64 hex characters.

Seed inputs vary by creator:

  • Task threadsSHA-256(alias + nonce); the CLI generates the nonce at spl task add time.
  • Dispatch threadsSHA-256(parent_thread + target_actor + dispatch_timestamp_ms); each dispatch run has a fresh thread.
  • Reserved threads — hand-assigned canonical IDs baked into the kernel:
    • th_engine_config
    • th_actor_registry
    • th_namespace_registry
    • th_instance_registry
    • th_fleet_control
    • th_consent

Client code never invents a thread ID freely. Use a well-known constant, seed-derive through an SDK helper, or ask the server to mint one.

Act types

Eight types in two groups of four.

Coordination (human/agent/service writes)

TypePurposeTypical body fields
INTENDState a goal, open a thread.goal, task, assigned_to, domain, priority, labels, namespace
DORecord an action performed.tool, args, cancels, topic
KNOWRecord an observation, fact, or verdict.topic, observation, fulfills, verdict, judged_by_did
LEARNRecord an insight or configuration change.topic, rule, trigger, config

Effect (adapter/tool writes)

TypePurposeTypical body fields
GETRetrieve a value.key, result
PUTStore a value.key, value
CALLInvoke a tool or service.tool, cmd, provider, request, response
MAPTransform data.from, to, transform

Data types

TypeMeaning
SCALARA single value. The default for most records.
FORMULAA computation specification — the body carries an expression.
DISTRIBUTIONA probability distribution, interval, or range.
REFERENCEA pointer to external content (URL, hash, content-addressed blob).
MORPHISMA transformation function between types. Used by MAP.
VOIDNo value. Used by pure side-effect records.

Clock rules

  • Monotonically increasing per (actor, thread). Cannot go backwards.
  • Starts at 0 for an actor's first record on a thread.
  • Gaps allowed0, 1, 5, 7 is valid. Gaps don't imply missing records; they typically mean the actor wrote elsewhere in between.
  • Per-actor per-thread. Two actors on the same thread each have independent clock sequences.
  • Duplicate clocks return DuplicateClock on ingest. This is the retry-safe idempotency hook — if you see this error, either your client's clock is wrong or there's a concurrent writer. Increment and retry.
  • Clocks are i64. Values from 0 to 9,223,372,036,854,775,807 are valid. Long-running threads well outside normal use before overflowing.

Wire format

On the HTTP API, records are JSON. The 8 fields appear at the top level:

{
  "parents": [],
  "thread": "th_a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890",
  "actor": "did:sync:user:alice",
  "act": "INTEND",
  "body": {
    "goal": "Deploy the authentication service v2",
    "namespace": "acme-corp/auth/prod"
  },
  "clock": 0,
  "data_type": "SCALAR",
  "judged_by": null
}

The response always echoes the stored record plus an id field:

{
  "parents": [],
  "thread": "th_a1b2c3d4…",
  "actor": "did:sync:user:alice",
  "act": "INTEND",
  "body": { "goal": "…", "namespace": "…" },
  "clock": 0,
  "data_type": "SCALAR",
  "judged_by": null,
  "id": "a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890"
}

The id is never sent by the client and never stored in the hash pre-image — it's purely the output of the hash function, shown for convenience.

Common body conventions

The kernel treats body as opaque, but conventions are stable enough to document.

Opening a task thread:

{
  "act": "INTEND",
  "body": {
    "task": true,
    "alias": "SKL-0334",
    "goal": "Docs depth pass",
    "assigned_to": "did:sync:agent:dev",
    "domain": "code",
    "priority": "high",
    "labels": ["v0.18-foundation", "docs"],
    "namespace": "default"
  }
}

Closing a thread with a verdict:

{
  "act": "KNOW",
  "body": {
    "fulfills": "3292991f311ea69ce02fa290c7517d0bd20658d0f4d379ffdeb39b52180f87ec",
    "verdict": "accept",
    "summary": "All deliverables shipped; build clean."
  },
  "judged_by": "3292991f311ea69ce02fa290c7517d0bd20658d0f4d379ffdeb39b52180f87ec"
}

A routing rule (LEARN on th_engine_config):

{
  "act": "LEARN",
  "thread": "th_engine_config",
  "body": {
    "topic": "routing_rule",
    "name": "code-to-dev",
    "expression": "record.act == 'INTEND' && record.body.domain == 'code'",
    "target": "did:sync:agent:dev",
    "priority": 100
  }
}

A correction record:

{
  "act": "KNOW",
  "body": {
    "topic": "correction",
    "supersedes": "<bad_record_id>",
    "reason": "value was wrong; correct value is X"
  },
  "parents": ["<bad_record_id>"]
}

A cancellation:

{
  "act": "DO",
  "body": {
    "cancels": "<intend_id>",
    "reason": "requirements changed"
  }
}

See CEL expressions for the full set of topics the engine recognizes, and the body.kind grammar (ADR-034) for the SDK-enforced structure of body.kind in integration records.

What the kernel validates at ingest

The kernel validates a small, fast set:

  1. All 8 fields present with the correct types.
  2. thread matches ^th_[0-9a-f]{64}$.
  3. actor is a syntactically valid DID. Unknown actors are allowed; invalid syntax is rejected.
  4. act is one of the 8 types.
  5. data_type is one of the 6 types.
  6. clock is a non-negative i64.
  7. parents is a sorted-ascending array of valid record IDs (64-hex each).
  8. body.namespace (if present) passes namespace narrowing — every ancestor must exist in the registry and be Active.

What the kernel does not validate:

  • Semantic correctness of body. The kernel doesn't know whether body.fulfills refers to a real record, whether body.topic is a recognised topic, or whether the body shape fits the declared data_type. All of that is consumer-side.
  • Signature verification. Signatures live in the federation layer; local records are not signed.
  • Duplicate content (apart from duplicate clocks). Two records differing only in clock are two distinct records; that's allowed.

Errors

The POST /v1/records endpoint returns structured JSON errors:

CodeMeaning
INVALID_SHAPE8 fields missing, wrong types, or malformed thread/actor/act/data_type.
DUPLICATE_CLOCKAnother record with the same (actor, thread, clock) already exists. Retry with clock+1.
NAMESPACE_REJECTEDbody.namespace (or an ancestor) is missing or non-Active in the registry.
AUTH_REQUIREDauth.required=true and no valid bearer token was presented.
FORBIDDENA CEL permission rule denied the write.

Error responses include the failing field and, where applicable, a recovery hint. See API reference for the full error shape.

What's next

On this page