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 define what agents can do in a space and under what conditions. They manage state machines over expressions — defining states, transitions, and the actions that trigger them.
Flows still use link-based state tracking internally. The flow state is determined by querying links in the perspective, and actions modify links to change state.
Here's a complete example of a Todo flow:
// Define flow states and transitions
const todoFlow = {
name: "TODO",
states: {
ready: 0,
doing: 0.5,
done: 1
},
// State is determined by todo://state links
stateQuery: (base) => `todo://state`,
transitions: [
{
from: "ready",
to: "doing",
action: "Start",
effects: [
{ action: "addLink", source: "this", predicate: "todo://state", target: "todo://doing" },
{ action: "removeLink", source: "this", predicate: "todo://state", target: "todo://ready" }
]
},
{
from: "doing",
to: "done",
action: "Finish",
effects: [
{ action: "addLink", source: "this", predicate: "todo://state", target: "todo://done" },
{ action: "removeLink", source: "this", predicate: "todo://state", target: "todo://doing" }
]
}
]
};This flow defines:
- A named flow ("TODO") with states (0 = ready, 0.5 = doing, 1 = done)
- How states are determined (by checking todo://state links)
- Available actions for each state with their transitions and effects
Using Flows in Code
// Get all flows defined in a perspective
const flows = await perspective.sdnaFlows();
// Returns: ["TODO"]
// Check what flows are available for an expression
const availableFlows = await perspective.availableFlows("expression://123");
// Start a flow on an expression
await perspective.startFlow("TODO", "expression://123");
// Get expressions in a specific flow state
const readyTodos = await perspective.expressionsInFlowState("TODO", 0);
// Get current state of an expression in a flow
const state = await perspective.flowState("TODO", "expression://123");
// Returns: 0, 0.5, or 1
// Get available actions for current state
const actions = await perspective.flowActions("TODO", "expression://123");
// Returns: ["Start"] if in ready state
// Execute an action
await perspective.runFlowAction("TODO", "expression://123", "Start");
// Transitions from ready (0) to doing (0.5)Adding Flows to a Perspective
await perspective.addSdna("Todo", flowDefinition, "flow");
const flows = await perspective.sdnaFlows();
console.log(flows); // Should include "TODO"Query Engines: SurrealDB & Prolog
AD4M provides two query engines for working with Social DNA and perspective data:
SurrealDB: High-Performance Queries (Recommended)
For most query needs, SurrealDB provides 10-100x faster performance than Prolog. It's especially powerful for:
- Fast filtering and searching
- Graph traversal queries using indexed
in.uriandout.urifields - Aggregations and analytics
- Large dataset handling
// Fast query - find all todos in "done" state
const doneTodos = await perspective.querySurrealDB(
"SELECT * FROM link WHERE predicate = 'todo://state' AND target = 'todo://done'"
);
// Graph traversal - find all posts by Alice
const alicePosts = await perspective.querySurrealDB(
"SELECT target FROM link WHERE in.uri = 'user://alice' AND predicate = 'authored'"
);
// Aggregation - count by type
const stats = await perspective.querySurrealDB(
"SELECT predicate, count() as total FROM link GROUP BY predicate"
);Performance Tip: Use in.uri (source) and out.uri (target) for graph traversal - they're indexed.
Note: Ad4mModel uses SurrealDB by default for findAll() and query builder operations. See the SurrealDB Queries Guide for comprehensive examples.
The Prolog Engine (Legacy)
The Prolog engine is maintained for backward compatibility. For new development,
use SurrealDB queries or the Ad4mModel system (which uses SurrealDB by default).
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 SurrealDB 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 with SurrealDB (recommended, 10-100x faster)
const activeTodos = await Todo.findAll(perspective, {
where: { state: "active" }
});
// Option 2: Direct SurrealQL query
const todos = await perspective.querySurrealDB(
"SELECT * FROM link WHERE predicate = 'todo://state' AND target = 'active'"
);
// Option 3: Prolog (legacy, 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