The pflow ecosystem currently uses three JSON-LD vocabularies. Petri net models reference pflow.xyz/schema. Blog posts carry schema.org metadata. Federation speaks ActivityStreams. Three different contexts, authored by three different communities, consumed by different software — yet they all share the same envelope. That convergence is not accidental. JSON-LD earns its place as infrastructure because it is purely declarative and monotonically expansive: two properties that matter more than any feature list.
Here is a trimmed snippet from each context as it appears in production.
Petri net model (pflow.xyz editor):
{
"@context": "https://pflow.xyz/schema",
"@type": "PetriNet",
"places": {
"Idle": { "@type": "Place", "initial": [1], "capacity": [1] }
},
"transitions": {
"Brew": { "@type": "Transition" }
},
"arcs": [
{ "@type": "Arrow", "source": "Idle", "target": "Brew", "weight": [1] }
]
}
Blog metadata (auto-generated by tens-city):
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "JSON-LD as Declarative Infrastructure",
"author": { "@type": "Person", "name": "stackdump" },
"datePublished": "2026-02-15T00:00:00Z"
}
ActivityPub actor (federation profile):
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type": "Person",
"preferredUsername": "myork",
"inbox": "https://blog.stackdump.com/users/myork/inbox",
"publicKey": { "id": "...#main-key", "publicKeyPem": "..." }
}
Each snippet is self-describing. Each links to a vocabulary that defines its terms. And none of them contain instructions — they are pure assertions.
A JSON-LD document is a serialized RDF graph: a set of subject-predicate-object triples. It makes statements about the world but never tells you what to do with them. There are no callbacks, no event handlers, no conditionally-included fields.
This matters for interoperability. The same .jsonld Petri net file is consumed by at least three independent interpreters:
go-pflow reads the same file for ODE-based analysis and validation.petri-pilot compiles it into executable service modules.None of these consumers coordinate with each other. They do not need to. The file is assertions, not a protocol. Each consumer extracts the triples it understands and ignores the rest. A code generator does not care about x/y layout coordinates; the visual editor does not care about Seal metadata. Declarative data degrades gracefully by design.
Contrast this with imperative serialization — formats where the order of fields implies a processing sequence, or where consumers must execute embedded logic to reconstruct the data. JSON-LD sidesteps all of that. The @context resolves local terms to global IRIs. The consumer interprets the graph. Nothing in between.
The pflow.xyz schema has grown three times since its introduction:
PetriNet, Place, Transition, Arrow, PersonSeal, Invariant, Guard, TokenSetCompositeNet, NetType, Link, InterfaceEach expansion introduced new terms. No existing term was removed or redefined. A model authored in 2024 using only PetriNet, Place, Transition, and Arrow remains valid under the 2026 schema — not because we tested backwards compatibility, but because the schema only grows.
This is monotonic expansion: new facts can be added, but existing facts are never retracted. It is the same discipline that makes append-only logs reliable and RDF graphs composable. In a monotonic system, learning more never invalidates what we already know.
Practically, this means old models never break. A Petri net saved before Seal existed still loads in an editor that understands seals. The editor simply sees a net without seal metadata — a valid state. No migration scripts, no version negotiation, no "this file was created with an older version" warnings. Backwards compatibility emerges from structure, not policy.
If JSON-LD is declarative, its identity should derive from what it says, not from how it was serialized. Two documents that make the same assertions — regardless of key order, whitespace, or field arrangement — should hash to the same value.
The URDNA2015 algorithm (RDF Dataset Normalization) makes this possible. It converts any JSON-LD document into a canonical set of N-Quads — sorted, deterministic, order-independent. From there, a standard hash produces a content identifier.
In pflow-xyz, the sealing pipeline implements this directly:
// From seal.go — simplified
proc := ld.NewJsonLdProcessor()
opts := ld.NewJsonLdOptions("")
opts.Format = "application/n-quads"
opts.Algorithm = "URDNA2015"
normalized, _ := proc.Normalize(doc, opts) // canonical N-Quads
multihash, _ := mh.Sum([]byte(normalized.(string)), mh.SHA2_256, -1)
cid := cid.NewCidV1(cid.DagJSON, multihash) // CIDv1 with base58btc
The resulting CID becomes the model's @id — a self-certifying identifier. If the model changes, the CID changes. If two independently-created models happen to describe the same graph, they get the same CID. Identity follows from content, not from a registry or a counter.
This property is what makes seals in Categorical Net Types trustworthy: the seal is a commitment to a specific graph, and any party can verify it by re-canonicalizing and re-hashing.
There is a clean categorical reading of what @context does. A JSON-LD context is a mapping from local terms (short names like Place, Transition) to global IRIs (like https://pflow.xyz/schema#Place). This is a functor:
@context : LocalTerms → GlobalIRIs
It maps the local category of terms used in a document to the global category of well-defined identifiers. Extending a context — adding new term mappings while preserving existing ones — is a natural transformation between functors: the old mapping still holds, and new mappings are layered on top.
The three vocabularies we use (pflow.xyz, schema.org, ActivityStreams) compose as a coproduct. Each vocabulary contributes its terms to the combined graph without collision, because the IRIs are namespace-disjoint. A document can reference all three contexts simultaneously, and the terms resolve unambiguously.
Content addressing then gives us identity in this category. Two objects (JSON-LD documents) are equal if and only if their canonical forms are equal — which is exactly what a content-addressed identifier witnesses.
JSON-LD, categorically:
@contextis a functor from local names to global identifiers. Schema extension is a natural transformation. Independent vocabularies compose as coproducts. Canonicalization provides identity.
Three properties do the work. Declarative is what makes a document safe to share: it contains assertions, never instructions, so each consumer takes what it needs and ignores the rest. Monotonic is what makes it safe to extend: adding terms never breaks an existing document, so backwards compatibility is structural rather than something you test for. Canonical is what makes it content-addressable: URDNA2015 fixes graph identity independently of serialization order, which is what lets an identifier certify itself. If you want the categorical gloss, @context behaves like a functor from local terms to global IRIs, and extending a context preserves the existing mappings — I'll call that a natural transformation and leave the coherence checking as an exercise. That is why pflow.xyz/schema, schema.org and ActivityStreams coexist here with zero coordination between them.
JSON-LD is infrastructure, and it works because it does less, not more: it asserts, it expands, it canonicalizes. Everything else is the consumer's problem, and that division of responsibility is what makes the composition possible.