Social DNA: Making Social Interaction Patterns Explicit
What is Social DNA?
Social DNA represents the core interaction patterns and social contracts within a digital space. Just as biological DNA encodes the rules for how cells interact and function, Social DNA encodes the rules for how agents (users) interact and collaborate in a digital space.
Think of it as the "business logic" of social applications, but with a crucial difference: instead of being buried in application code, these patterns are made explicit and separated from both:
- The storage layer (Languages) - how data is actually stored and shared
- The UI layer - how these interactions are presented to users
Why "Social DNA"?
In traditional applications, social interaction patterns are often:
- Implicit in the application code
- Tied to specific storage implementations
- Mixed with UI concerns
- Hard to modify or extend
- Not portable between applications
Social DNA makes these patterns:
- Explicit and declarative
- Independent of storage details
- Separated from UI concerns
- Easy to modify and extend
- Portable between applications
Example: A Simple Social Space
Let's look at a common social interaction pattern: posting and liking content. In a traditional app, this might be scattered across:
- Database schemas
- API endpoints
- UI components
- Business logic
With Social DNA, we can express this as a clear social contract:
// Define what a "post" means in this space
const postClass = {
properties: {
content: "string",
author: "did",
timestamp: "datetime"
},
actions: {
like: "any agent can like a post once",
comment: "any agent can comment on a post",
edit: "only the author can edit their post"
}
}
// Define what "trending" means in this space
const trendingRule = {
condition: "post has more than 5 likes in last 24 hours",
action: "mark post as trending"
}This is just pseudo-code, but it illustrates how Social DNA makes interaction patterns explicit and declarative.
How Social DNA Works
AD4M implements Social DNA through two main mechanisms:
-
Subject Classes: Define what things mean in a space
- Graph patterns that represent specific types of data
- Properties and relationships between data
- Validation rules for data integrity
-
Flows: Define what agents can do in a space
- Preconditions for actions
- State transitions
- Effects of actions
Subject Classes are defined using SHACL (Shapes Constraint Language), a W3C standard for describing and validating RDF graphs. SHACL is a natural fit for AD4M because:
- It's a standard for describing graph shapes — exactly what perspectives are
- It's declarative and machine-readable
- It supports property constraints, cardinality, and data types
- It enables interoperability with the broader semantic web ecosystem
You typically won't write SHACL by hand. AD4M provides Model Classes — TypeScript decorators that automatically generate SHACL definitions from your class declarations. (Think of ORMs vs raw SQL. See Model Classes for details.)
Subject Classes: Defining Things
Understanding Subject Classes
The term "Subject Class" was deliberately chosen to emphasize the subjective nature of pattern recognition in semantic graphs. Unlike traditional object-oriented programming where classes define objective structures, Subject Classes represent subjective interpretations of graph patterns – different ways of "seeing" meaning in the connections between expressions.
Subjective Pattern Recognition
Think of a Subject Class as a lens through which an application views and interprets graph patterns:
- Different apps can have different "opinions" about what constitutes a meaningful pattern
- The same base expression can be interpreted through multiple Subject Classes
- Each Subject Class defines what properties and relationships are important to its perspective
For example, consider a base expression representing some content:
expression://xyz123Different applications might interpret this through different Subject Classes:
- A chat app might see it as a "Message" with replies and reactions
- A task app might see it as a "Todo" with state and assignments
- A social app might see it as a "Post" with likes and shares
- A wiki might see it as a "Document" with citations and revisions
Each of these interpretations is equally valid – they're just different subjective lenses on the same underlying graph structure.
Subject-Oriented Programming
This approach is inspired by subject-oriented programming, where:
- Behavior and structure are separated from the base objects
- Different subjects can have different views of the same object
- Multiple interpretations can coexist without conflict
- New interpretations can be added without modifying existing ones
In AD4M, this means:
- Subject Classes define patterns around base expressions
- Multiple Subject Classes can match the same expression
- New Subject Classes can be added without changing existing ones
- Applications can choose which patterns matter to them
Base Expressions and Graph Patterns
Every Subject Class instance is anchored to a base expression, but its properties and relationships are defined by patterns in the surrounding graph:
Base Expression: expression://xyz123
│
┌───────────────┬──┴──┬───────────────┐
│ │ │ │
state author title comments
│ │ │ │
"done" did:123 "Hi" [expr1, expr2]Different Subject Classes might look for different patterns around this same base:
// A Todo class looks for state and assignments
class Todo {
state: string; // Looks for todo://state links
assignee: string; // Looks for todo://assigned-to links
}
// A Post class looks for social interactions
class Post {
likes: string[]; // Looks for social://like links
comments: string[]; // Looks for social://comment links
}
// Both can exist simultaneously on the same base expression
const todo = await perspective.getSubjectProxy<Todo>(baseExpr, "Todo");
const post = await perspective.getSubjectProxy<Post>(baseExpr, "Post");How Subject Classes are Defined (SHACL)
Subject Classes are stored as SHACL shapes in the perspective's link graph. A SHACL NodeShape describes a class, and PropertyShapes describe its properties.
Here's how a Todo class maps to SHACL:
@Model({ name: "Todo" })
class Todo extends Ad4mModel {
@Property({ through: "todo://state", initial: "todo://ready" })
state: string = "";
@Property({ through: "todo://has_title" })
title?: string;
@HasMany({ through: "todo://comment" })
comments: string[] = [];
}Key SHACL Concepts for AD4M
| SHACL Constraint | AD4M Meaning | Example |
|---|---|---|
sh:maxCount 1 | Scalar property (single value) | state, title, name |
No sh:maxCount | Collection (multiple values) | comments, tags, members |
sh:minCount 1 | Required property | Must be set on creation |
sh:datatype xsd:string | String value | Stored as literal:string:... |
sh:class <uri> | Reference to another Subject Class | Typed relationship |
ad4m://initial | Default value on creation | Initial state |
ad4m://resolveLanguage | Expression language for values | "literal" for inline values |
ad4m://readOnly | Property is read-only | Computed/immutable property |
Using Subject Classes in Code
While the PerspectiveProxy provides low-level methods for working with Subject Classes, we strongly recommend using our high-level Ad4mModel system (described in detail in the Model Classes guide). This TypeScript-based approach provides:
- Automatic SHACL generation from decorated classes
- Type safety and IDE support
- Rich ActiveRecord-style semantics
- Clean, declarative syntax
Here's how to define and use Subject Classes the recommended way:
import { Ad4mModel, Model, Property, HasMany } from '@coasys/ad4m';
@Model({ name: "Todo" })
class Todo extends Ad4mModel {
@Property({ through: "todo://state", initial: "todo://ready" })
state: string = "";
@Property({ through: "todo://has_title" })
title?: string;
@HasMany({ through: "todo://comment" })
comments: string[] = [];
}
// Register the class with a perspective (once at app startup)
await Todo.register(perspective);
// Use it with full type safety and ActiveRecord patterns
const todo = new Todo(perspective);
todo.state = "todo://doing";
await todo.save();
// Query with the fluent API
const todos = await Todo.findAll(perspective);
const doneTodos = await Todo.query(perspective)
.where({ state: "todo://done" })
.get();This approach automatically generates SHACL and provides a rich development experience.
Working with SHACL Directly
For advanced use cases, you can work with SHACL shapes directly using the SHACLShape class:
import { SHACLShape } from '@coasys/ad4m';
// Create a shape programmatically
const shape = new SHACLShape('recipe://Recipe');
shape.addProperty({
path: 'recipe://name',
datatype: 'xsd:string',
maxCount: 1,
minCount: 1,
});
shape.addProperty({
path: 'recipe://ingredient',
datatype: 'xsd:string',
// No maxCount = collection
});
// Store in perspective
await perspective.addShacl('Recipe', shape);
// Retrieve
const retrieved = await perspective.getShacl('Recipe');
// List all shapes
const allShapes = await perspective.getAllShacl();Understanding the Lower Level
For a complete understanding, here are the basic PerspectiveProxy methods that power the above abstractions:
// Add a subject class definition (SHACL) to a perspective
await Todo.register(perspective);
// Or add SHACL directly
await perspective.addShacl("Todo", shape);
// List all available subject classes
const classes = await perspective.subjectClasses();
// Returns: ["Todo"]
// Create a new subject instance
await perspective.createSubject("Todo", "expression://123");
// Check if an expression is an instance of a class
const isTodo = await perspective.isSubjectInstance("expression://123", "Todo");
// Get subject data
const todoData = await perspective.getSubjectData("Todo", "expression://123");
// Remove a subject (runs destructor if defined)
await perspective.removeSubject("Todo", "expression://123");
// Get all instances of a class
const allTodos = await perspective.getAllSubjectInstances("Todo");
// Get a proxy object
const todo = await perspective.getSubjectProxy("expression://123", "Todo");While these methods are available, we recommend using the Ad4mModel system for most use cases. It provides a more maintainable and type-safe way to work with Subject Classes while handling all the low-level details automatically.
Flows: Defining Actions
Flows describe how a group advances an expression through a state machine, with each transition backed by an on-graph FlowTransitionProposal and (optionally) a ConsensusRule gating when a proposal fires. Each FlowState carries an interpretationHint — a short English sentence the interpretation engine surfaces to the LLM so agent-authored proposals stay coherent with the flow's intent.
This section introduces flows as social DNA. The Flows page covers the engine: the on-graph objects, how state is derived rather than stored, proposeTransition / accept / reject, role gates including producedByFlow, and a worked end-to-end example.
Flow state is tracked on-graph via FlowInstance (per-subject state) and
FlowTransitionProposal (per-vote), not by the ad-hoc todo://state-style
links used in earlier iterations. See
docs/flow-interpretation-hints-design.md (opens in a new tab)
for the full v5 design.
Here's a Delivery flow expressed with the SHACLFlow class — the shape a group would share as social DNA:
import { SHACLFlow } from "@coasys/ad4m";
const deliveryFlow = new SHACLFlow("Delivery", "delivery://");
deliveryFlow.inputTypes = ["Task"];
deliveryFlow.interpretationHint =
"Advance a Task through delivery: Identified → Scoped → InProgress → Review → Done.";
deliveryFlow.addState({ name: "Identified", value: 0, interpretationHint: "The Task has been named but not yet scoped." });
deliveryFlow.addState({ name: "Scoped", value: 1, interpretationHint: "The Task has an acceptance criterion." });
deliveryFlow.addState({ name: "InProgress", value: 2, interpretationHint: "Someone is actively working on it." });
deliveryFlow.addState({ name: "Review", value: 3, interpretationHint: "The Task is ready for peer review." });
deliveryFlow.addState({ name: "Done", value: 4, interpretationHint: "The Task is accepted and complete." });
deliveryFlow.addTransition({ actionName: "Scope", fromState: "Identified", toState: "Scoped", actions: [] });
deliveryFlow.addTransition({ actionName: "Start", fromState: "Scoped", toState: "InProgress", actions: [] });
deliveryFlow.addTransition({ actionName: "Submit", fromState: "InProgress", toState: "Review", actions: [] });
deliveryFlow.addTransition({ actionName: "Accept", fromState: "Review", toState: "Done", actions: [] });
// Optional: require n distinct DIDs to co-sign a proposal before it fires.
deliveryFlow.consensusRule = { n: 2 };Using Flows in Code
import { FlowInstance } from "@coasys/ad4m";
// Which flows accept this expression as input? Returns flow names whose
// `inputTypes` match the expression's registered subject classes, plus any
// flow declaring no `inputTypes` or the "any" wildcard.
const availableFlows = await perspective.availableFlows("task://sprint-3/T-42");
// Returns: ["Delivery"]
// Mint a fresh FlowInstance in the flow's initial state (states[0] — the
// lowest-valued state). Takes the flow *name*; the shape is loaded for you.
const instance = await FlowInstance.start(
perspective,
"Delivery",
"task://sprint-3/T-42",
);
// Read every active FlowInstance for a flow. The filter also accepts
// `{ flowName?, subject? }` — pass `subject` to ask "what is running on
// this expression?".
const instances = await FlowInstance.findAll(perspective, "Delivery");
// Where is it now? `currentState` resolves to the FlowState on the shape,
// so the interpretationHint the LLM sees is right there.
console.log(instance.currentStateName); // "Identified"
console.log(instance.currentState.interpretationHint);
// Which actions are legal from here? Empty on a terminal state.
for (const t of instance.availableTransitions) {
console.log(t.actionName, "→", t.toState); // "Scope" → "Scoped"
}
// Proposals standing against this instance.
const proposals = await instance.proposals();That is the read surface. The write surface is on the same wrapper:
instance.proposeTransition(toState, rationale?) is the button click
(mint or co-sign), instance.acceptProposal(p) adds a signature to a
listed proposal, instance.rejectProposal(p) withdraws the links you
signed. Reject is not a cancel — retracting a settling vote moves the
flow back. See Flows for FlowProposeResult, role gates, and
a worked end-to-end example. The executable twin is
tests/js/tests/model/flow-task-handover.test.ts.
Adding Flows to a Perspective
// Recommended: type-safe helper takes the SHACLFlow instance directly.
await perspective.addFlow("Delivery", deliveryFlow);
const flows = await perspective.sdnaFlows();
console.log(flows); // Should include "Delivery"Query Engine: SPARQL
AD4M provides a high-performance SPARQL query engine for working with Social DNA and perspective data, powered by Oxigraph and executing entirely server-side in Rust.
SPARQL: Fast Graph Queries
SPARQL is the standard query language for RDF data and is the primary query engine in AD4M:
- Direct queries against the perspective's RDF graph
- Filtering, sorting, and aggregation
- Multi-hop graph traversal via pattern matching
- Server-side execution for optimal performance
// Find all todos in "done" state
const doneTodos = await perspective.querySparql(`
SELECT ?todo WHERE {
?link a rdf:Statement ;
rdf:subject ?todo ;
rdf:predicate <todo://state> ;
rdf:object <todo://done> .
}
`);
// Find all posts by Alice
const alicePosts = await perspective.querySparql(`
SELECT ?target WHERE {
?link a rdf:Statement ;
rdf:subject <user://alice> ;
rdf:predicate <authored> ;
rdf:object ?target .
}
`);
// Aggregation - count by type
const stats = await perspective.querySparql(`
SELECT ?predicate (COUNT(?link) AS ?total) WHERE {
?link a rdf:Statement ;
rdf:predicate ?predicate .
}
GROUP BY ?predicate
`);Note: Ad4mModel uses the SPARQL engine internally for findAll() and query builder operations. See the SPARQL Queries Guide for comprehensive examples.
The Prolog Engine (Backward Compatibility)
The Prolog engine is maintained for backward compatibility. For new development,
use SPARQL queries or the Ad4mModel system (which uses SPARQL internally).
For custom logic programming and backward compatibility, AD4M includes a Prolog engine:
Core Predicates
// Basic graph predicates
triple(Subject, Predicate, Object). // Access raw triples
link(Subject, Predicate, Object, Timestamp, Author). // Access links with metadata
// Graph traversal
reachable(A, B). // True if B can be reached from A through any predicatesWhen to use which?
- Use SPARQL for: Fast queries, graph traversal, analytics (most use cases)
- Use Prolog for: Complex logic rules, backward compatibility
Using Social DNA in Your Space
To add Social DNA to a Perspective:
// Recommended: Use Model Classes (auto-generates SHACL)
await Todo.register(perspective);
// Or add SHACL shapes directly
const shape = new SHACLShape('todo://Todo');
shape.addProperty({ path: 'todo://state', datatype: 'xsd:string', maxCount: 1, minCount: 1 });
await perspective.addShacl('Todo', shape);
// Add a flow definition
await perspective.addSdna("Todo", flowDefinition, "flow");Then query your data:
// Option 1: Use Ad4mModel (recommended — queries execute server-side via SPARQL)
const activeTodos = await Todo.findAll(perspective, {
where: { state: "active" }
});
// Option 2: Direct SPARQL query
const todos = await perspective.querySparql(`
SELECT ?todo WHERE {
?link a rdf:Statement ;
rdf:subject ?todo ;
rdf:predicate <todo://state> ;
rdf:object <active> .
}
`);
// Option 3: Prolog (for backward compatibility)
const todos = await perspective.infer(
'instance(Todo, "Todo"), property_getter("Todo", Todo, "state", "active")'
);Best Practices
-
Start with Patterns
- Identify common interaction patterns
- Make implicit rules explicit
- Think about preconditions and effects
-
Use Subject Classes for:
- Data validation
- Computed properties
- Relationship definitions
-
Use Flows for:
- Action permissions
- State transitions
- Complex processes
-
Keep it Simple
- Start with basic patterns
- Add complexity gradually
- Use the high-level Model Classes when possible
-
Prefer SHACL over Prolog
- SHACL is the standard for subject class definitions
- Model Classes generate SHACL automatically
- Prolog is maintained for backward compatibility only