What Is sdk-js and Why Should You Care?
Most developers stumble onto new SDKs because they're already building something and hit a wall. If you've been following the Astrid OS project, or you're curious about building modular, deployable units called capsules, then sdk-js is the piece you need on the JavaScript and TypeScript side of things.
sdk-js is the official JavaScript/TypeScript SDK for building Astrid OS capsules. It's developed under the unicity-astrid GitHub organization and is designed to work alongside sdk-rust, its Rust counterpart. You don't have to use both. But they share conventions, so the mental model transfers cleanly between them.
This article breaks down what capsules are, how sdk-js is structured, and how to start wiring one up in a TypeScript project. No hand-waving. Actual code.
Capsules: The Core Concept
Before touching any code, the concept of a capsule needs to land properly. A capsule in the Astrid OS model is a self-contained, deployable unit of logic. Think of it like a microservice, but lighter and more opinionated about its interface. It has a defined input, a defined output, and no hidden dependencies leaking out the sides.
This matters because the biggest pain point in multi-language or multi-runtime systems is the contract between pieces. Capsules enforce that contract at the boundary. That's the whole point. You write a capsule in TypeScript today, and if a Rust capsule needs to call it tomorrow, the interface is already agreed upon.
It's a bit like how zero-native bridges Zig and web UI layers by enforcing strict boundaries. Different tech, same underlying philosophy: keep the seams clean.
Setting Up sdk-js in a TypeScript Project
First, install the package. At the time of writing, the SDK is available directly from the GitHub repository. Check the repo for the latest published package name and version before installing.
# If published to npm:
npm install @unicity-astrid/sdk-js
# Or install directly from GitHub:
npm install github:unicity-astrid/sdk-js
For TypeScript support, make sure your tsconfig.json has "moduleResolution": "node" or "bundler" depending on your setup, and that strict mode is on. The SDK is typed, so you want the compiler to catch mismatches early.
Defining a Capsule
Here's the basic shape of a capsule definition using sdk-js. The SDK exposes a defineCapsule function (or equivalent, depending on the version you're using) that takes a descriptor and a handler.
import { defineCapsule } from '@unicity-astrid/sdk-js';
const greetCapsule = defineCapsule({
name: 'greet',
version: '1.0.0',
input: {
name: 'string',
},
output: {
message: 'string',
},
handler: async ({ input }) => {
return {
message: `Hello, ${input.name}!`,
};
},
});
export default greetCapsule;
A few things worth noting here. The name and version fields aren't decorative. They're part of the capsule's identity in the Astrid OS runtime. If you rename a capsule without bumping the version, callers might break silently. Treat them like a public API surface.
The input and output fields describe the schema. The SDK validates these at the boundary, so bad data gets caught before it reaches your handler. That alone saves a lot of defensive boilerplate inside business logic.
TypeScript Types and Schema Inference
One of the places sdk-js genuinely shines is how it handles TypeScript inference. If your schema is declared correctly, the input argument inside the handler is fully typed. You don't need to write a separate interface and then hope it stays in sync with the schema definition.
import { defineCapsule } from '@unicity-astrid/sdk-js';
const addCapsule = defineCapsule({
name: 'add',
version: '1.0.0',
input: {
a: 'number',
b: 'number',
},
output: {
result: 'number',
},
handler: async ({ input }) => {
// TypeScript knows input.a and input.b are numbers here
return {
result: input.a + input.b,
};
},
});
export default addCapsule;
This is the same pattern you'd appreciate in a TypeScript React setup where inference eliminates a whole category of type annotation chores. The principle is identical: define the shape once, let the compiler do the rest.
Connecting to the Astrid OS Runtime
Defining a capsule is half the work. The other half is registering it so the Astrid OS runtime can actually call it. The SDK provides a runtime client for this.
import { AstridRuntime } from '@unicity-astrid/sdk-js';
import greetCapsule from './capsules/greet';
import addCapsule from './capsules/add';
const runtime = new AstridRuntime();
runtime.register(greetCapsule);
runtime.register(addCapsule);
runtime.start().then(() => {
console.log('Capsule runtime is live');
});
The AstridRuntime class handles the plumbing: discovery, routing, and lifecycle management. You register capsules, start the runtime, and it takes it from there. You can register as many capsules as you want in a single runtime instance.
One thing to plan for early: capsule naming collisions. If two capsules share the same name and version, the runtime will throw on startup. Keep a naming convention from day one, something like domain/action (auth/login, payments/charge). It's much easier to enforce that now than to untangle it later.
How sdk-js and sdk-rust Work Together
The multi-language angle is what makes the Astrid OS SDK ecosystem interesting. sdk-js and sdk-rust don't share code, but they share a protocol. A capsule written in Rust exposes the same interface contract as one written in TypeScript. That means a TypeScript runtime can call a Rust capsule and vice versa, as long as both follow the schema.
In practice this means your team can pick the right language for each capsule without building a bespoke bridge every time. A CPU-bound processing capsule makes sense in Rust. A capsule that glues together API calls makes sense in TypeScript. The Astrid OS runtime handles dispatch. You don't write the glue.
This kind of boundary-first design philosophy is increasingly common in newer developer tooling. It's the same thinking behind tools like Bumblebee's read-only endpoint scanning, where explicit interfaces prevent unexpected side effects across system boundaries.
Error Handling Inside Capsules
The handler's async nature means errors are promises that can reject. But don't rely on unhandled rejections. The SDK expects you to either return a valid output or throw a typed error that the runtime can serialize back to the caller.
import { defineCapsule, CapsuleError } from '@unicity-astrid/sdk-js';
const divideCapsule = defineCapsule({
name: 'divide',
version: '1.0.0',
input: {
a: 'number',
b: 'number',
},
output: {
result: 'number',
},
handler: async ({ input }) => {
if (input.b === 0) {
throw new CapsuleError('DIVISION_BY_ZERO', 'Cannot divide by zero');
}
return {
result: input.a / input.b,
};
},
});
export default divideCapsule;
Using CapsuleError (or the SDK's equivalent error class) instead of a plain Error gives the runtime enough information to return a structured error response. The caller gets a meaningful error code, not a 500 with an empty body. That's the difference between a system that's debuggable and one that isn't.
Testing Capsules Without a Full Runtime
You shouldn't need to spin up a full Astrid OS runtime to unit test a capsule. The handler is just an async function. Call it directly.
import greetCapsule from './capsules/greet';
async function testGreet() {
const result = await greetCapsule.handler({
input: { name: 'Ada' },
});
console.assert(result.message === 'Hello, Ada!', 'Greeting mismatch');
console.log('Test passed:', result.message);
}
testGreet();
Wire this into Jest, Vitest, or whatever test runner you prefer. The capsule handler is pure enough that there's no mocking ceremony. Input in, output out. That's it.
What to Watch Out For
sdk-js is a young project. A few practical cautions:
- Schema changes are breaking changes. Once another capsule or runtime depends on your schema, adding required fields or removing fields is a breaking change. Version bumps exist for exactly this reason. Use them.
- The GitHub repo is the source of truth. Package names, API surfaces, and conventions can shift between releases. Before building anything production-facing, read the latest README at github.com/unicity-astrid/sdk-js for any changes since this was written.
- TypeScript strict mode isn't optional here. The SDK's type inference does the most work when
strict: trueis set. Running without it isn't dangerous, but you'll miss the best part of the DX.
It's also worth acknowledging that sdk-js sits inside a broader ecosystem that's still taking shape. That's not a knock. Early adoption of well-designed tools pays off. But go in with eyes open. Build capsules that are easy to replace, not ones that assume the SDK's API is frozen forever.
Start Small, Then Expand
The best first capsule is the most boring one you can think of. A string formatter. A date converter. Something with obvious inputs and obvious outputs. Get the registration, testing, and error-handling patterns working on something trivial before you attach business-critical logic to the runtime.
Once you have one capsule shipping cleanly, the pattern repeats. The SDK's consistent structure means adding the tenth capsule takes about as long as adding the second. That's the compounding return on a well-designed SDK, and it's what makes the investment in learning sdk-js worth making now rather than later.
Ask yourself: what piece of logic in your current project has the cleanest input/output boundary? That's your first capsule.





