A starting point for building new AD4M expression languages using the modern ALDK (@coasys/ad4m-ldk) pattern.
An expression language owns a URI scheme and resolves URIs in that scheme to Expression records. Examples in the AD4M ecosystem:
- literal —
literal://json:<inline>,literal://string:<text>,literal://number:<n>for primitive values inlined in the URI. - language-language —
Qm…content-addresses of published language bundles. - git-expression-language —
git+https://<host>/<o>/<r>.git#<ref>:<path>for files at specific commits in any Git host.
This template gives you a working expression-language skeleton with:
defineLanguage({ expression: { get, isImmutable, … } })wiring with the modern ALDK- Example URI parser (
src/uri.ts) — adapt the regex to your scheme - Transport + Storage + Runtime + Signing adapter pattern (pure / impure separation) for testability
- Template variable support for per-instance configuration
- esbuild bundling for the Deno executor runtime
- Unit tests that run outside the executor (Node.js + tsx)
- Deno (v1.32+) — used by the executor runtime and the build script
- Node.js (v20+) + npm/pnpm — for dev dependencies and running tests
@coasys/ad4m-ldk— either:- Cloned at a sibling path:
../ad4m/ad4m-ldk/js/(the default) - Or set
AD4M_LDK_ENTRYenv var to the compiledlib/index.js
- Cloned at a sibling path:
# Install dev dependencies
pnpm install # or npm install
# Build the bundle
deno run --allow-all esbuild.ts
# Run tests
node --experimental-vm-modules --import tsx --test tests/*.test.ts
# Type-check
npx tsc --noEmit├── index.ts # Main entry — uses defineLanguage() from @coasys/ad4m-ldk
├── esbuild.ts # Build script (Deno + esbuild)
├── package.json # Dependencies
├── tsconfig.json # TypeScript config
├── src/
│ ├── types.ts # DID, Expression, ExpressionProof
│ ├── uri.ts # Example URI parser — replace with your scheme
│ ├── adapters.ts # Transport / Storage / Runtime / Signing interfaces
│ └── adapters-deno.ts # Deno-side adapter impls wrapping ad4m:host
├── tests/
│ └── uri.test.ts # Sample tests for the parser
├── build/ # Output directory (bundle.js)
├── README.md
└── LICENSE # CAL-1.0
| Link language | Expression language | |
|---|---|---|
| Capability | commit + sync + query (perspective-bound) |
expression (URI-bound) |
| State | Owns a Perspective; carries link expressions | Stateless resolver over a URI scheme |
| Lifecycle | init, then per-perspective context |
init, then per-call resolution |
| Output | PerspectiveDiff, link query results |
Expression<T> |
| Examples | git-link-language, nostr-link-language, p-diff-sync (Holochain) |
literal, language-language, git-expression-language |
An AD4M agent typically uses one Link Language per Neighbourhood and many Expression Languages — every URI scheme an app dereferences needs an Expression Language registered for it.
Core logic stays free of runtime-specific imports:
- Pure modules (
types.ts,uri.ts,adapters.ts) — noad4m:hostimports. Depend only on TypeScript types + injected adapters. Testable in plain Node.js. - Impure modules (
adapters-deno.ts) — wrapad4m:hostfunctions via@coasys/ad4m-ldk. Only imported inindex.tsduringinit().
This separation means your resolver logic can run in node --test without any AD4M runtime.
The transport layer abstracts HTTP calls. In the executor, httpFetch from ad4m:host is the only outbound network primitive.
Important: httpFetch returns raw body text on 2xx and throws on non-2xx. The DenoTransport in src/adapters-deno.ts handles this by parsing the error message format to extract status codes.
In tests, inject a MockTransport that implements the Transport interface.
For resolvers that cache content, the storage layer wraps the executor's KV store (storageGet, storagePut, storageDelete, storageListKeys). In tests, use a simple Map-based mock.
Note: at the time of writing, the executor's File I/O extension is not always installed, so the KV is in-memory only in some builds. Design caches to tolerate "lost on restart".
The modern ALDK entry point. Instead of the old create(context: LanguageContext) pattern, you call defineLanguage() with a config object declaring your capabilities:
const language = defineLanguage({
name: "my-expression-language",
version: "0.1.0",
isPublic: true,
async init() { /* ... */ },
expression: {
async get(address: string) { /* return Expression or null */ },
async isImmutable(address: string) { /* return boolean */ },
// Optional — most expression languages don't implement these:
// async create(content): Promise<string> { ... }
// async addressOf(content): Promise<string> { ... }
},
expressionUI: {
icon: () => "<svg>...</svg>",
constructorIcon: () => "<svg>...</svg>",
},
});The returned object has flat exports (expressionGet, expressionIcon, etc.) that the executor binds to.
Expression languages typically do not AD4M-sign the content they resolve. The Expression's proof is empty, and the underlying substrate's own integrity model (content addressing, host TLS, server-side ACLs) is what the consumer trusts. Document this clearly in your language's README — apps that need stronger guarantees can wrap your URIs in signed Link Expressions.
Template variables allow per-instance configuration at publish time. Mark them with the //!@ad4m-template-variable comment:
//!@ad4m-template-variable
const MY_CONFIG = "<to-be-filled>";When the language is published via language.publish, the executor replaces "<to-be-filled>" with actual values from the templateParams map. The possibleTemplateParams export tells the executor which variables exist.
Add your own template variables for backend URLs, API keys, cache sizes, or any per-instance configuration.
To publish your language to an AD4M executor:
- Build the bundle:
deno run --allow-all esbuild.ts - Use the executor's WebSocket API to call
language.publish:
{
"languagePath": "./build/bundle.js",
"languageMeta": {
"name": "my-ad4m-expression-language",
"description": "My custom expression language",
"possibleTemplateParams": ["UNIQUE_SEED"],
"sourceCodeLink": "https://github.com/your-org/your-repo"
}
}- Define the URI grammar in
src/uri.ts. Aim for an existing canonical form (RFC 3986, npm convention, web URLs) rather than inventing a new one when possible. - Replace
index.ts'sexpression.getbody with your resolution logic. - Set
isImmutablecorrectly — important for caching downstream. SHA-pinned URIs, content hashes, and other deterministic identifiers are immutable. - Decide whether
expression.createmakes sense for your scheme. For resolvers over existing content (HTTP fetchers, Git readers), it usually doesn't. - Add tests for the parser and the resolver. Mock the transport so the suite has no network dependency.