Tardigrade is a TypeScript framework for building durable, event-driven actors. Built on Effect.
How it works
Every event is written to an append-only log. Durable atoms reduce those events into state. Effect atoms propose effects based on current state. The runtime executes those effects and appends new events to the log.
Why Tardigrade?
| Durable | State is derived purely from an immutable event log. Actors survive crashes and resume automatically from where they stopped. |
|---|---|
| Composable | Build complex actors from very simple primitives. Actors are made of atoms and they infinitely compose with each other. |
| Scalable | Each actor runs on a single durable object. Scale from one to millions of actors seamlessly. |
| Native observability | With a log at the core, and behaviors defined as pure functions, you can easily inspect why your actors reached a particular state. |
Use Tardigrade
To run and deploy an agent, follow the Agent guide.
Quickstart
Install the latest Tardie CLI.
bun add -g tardie@latestLoad the quickstart template. This initialises a simple counter actor along with a SKILL.md for your agent.
tdg init counter
cd counter
Store state in a durable atom
An atom is the smallest state primitive in Tardigrade. When a new event is received, it reduces the event log into state that survives restarts.
import { Schema } from "effect"
import { durableAtom, event } from "tardie/core"
const Incremented = event({ type: "Incremented" })
const Decremented = event({ type: "Decremented" })
const makeCount = () => durableAtom({
name: "count",
input: Schema.Union([Incremented, Decremented]),
schema: Schema.Int,
initial: 0,
reduce: (count, event) => event.type === "Decremented" ? count - 1 : count + 1,
})
Perform actions using effect atoms
Effect atoms react to state changes by proposing real world actions. The runtime executes these action using the implementation you supply.
import { Effect } from "effect"
import { act, effectAtom, type Atom } from "tardie/core"
const Notify = act({
name: "notify",
input: Schema.Int,
success: Schema.Null,
failure: Schema.String,
})
export const notifyLayer = Notify.layer(count =>
Effect.log(`${count} is even`).pipe(Effect.as(null))
)
const notify = (count: Atom<number>) => effectAtom(get => ({
view: get(count),
events: {},
acts: get(count) > 0 && get(count) % 2 === 0
? { notify: Notify.request({ input: get(count) }) }
: {},
}))
Supply notifyLayer to the host to run the notification effect. In this example, the notifyLayer logs in the console whenever the count is an even number.
Define methods
Methods are how the world interacts with your actor. It defines a typed input and output schema, and a way to declare the status of the method call for an observor.
import { actorMethod } from "tardie/core"
const counterMethods = (count: Atom<number>) => ({
increment: actorMethod({
inputSchema: Schema.Null,
outputSchema: Schema.Int,
onReceive: Incremented.from(() => ({})),
result: (_input, get) => ({
status: "completed",
output: get(count),
}),
}),
decrement: actorMethod({
inputSchema: Schema.Null,
outputSchema: Schema.Int,
onReceive: Decremented.from(() => ({})),
result: (_input, get) => ({
status: "completed",
output: get(count),
}),
}),
})
Compose atoms into an actor
Return the composed atom along with methods for interaction to define your actor.
import { defineActor } from "tardie/core"
export const counter = defineActor("counter", Effect.sync(() => {
const count = makeCount()
const notifications = notify(count)
return {
atom: notifications,
methods: counterMethods(count),
}
}))
Run Tardigrade
Run Tardigrade locally or host it in a cloud of your choice as durable objects.
Local
Run Tardigrade as a HTTP server by providing a storage location and service implementation for the actor's requirements.
import { Context, Effect } from "effect"
import { createBunHost, methodHttp } from "tardie/platform/bun"
import { counter, notifyLayer } from "./actor"
const host = await Effect.runPromise(createBunHost({
actor: counter,
actorContext: Context.pick(),
storage: ".tardigrade",
services: () => notifyLayer,
}))
Bun.serve({
hostname: "127.0.0.1",
port: 4242,
fetch: methodHttp(host),
})
bun run server.ts
Cloud
Run Tardigrade actors as Durable Objects on Cloudflare or self-host on your own cloud using Celld. Both use the same Worker entrypoint and configuration.
import { Context } from "effect"
import { createActorWorker } from "tardie/platform/cloudflare"
import { counter, notifyLayer } from "./actor"
const worker = createActorWorker({
actor: counter,
actorContext: Context.pick(),
services: () => notifyLayer,
})
export const ActorDO = worker.ActorObject
export const ThreadDO = worker.ThreadObject
export default { fetch: worker.fetch }
wrangler.jsonc
{
"name": "counter",
"main": "worker.ts",
"compatibility_date": "2026-11-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{ "name": "ACTORS", "class_name": "ActorDO" },
{ "name": "THREADS", "class_name": "ThreadDO" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["ActorDO", "ThreadDO"] }
]
}
Deploy to Cloudflare with the Wrangler CLI:
bun add -d wrangler
bunx wrangler deploy
For self-hosting, install the Celld CLI and deploy to your fleet's storage bucket. Celld uses esbuild to bundle the Worker:
bun add -g esbuild
celld deploy . --bucket s3://YOUR-BUCKET
Start your Celld nodes against the same bucket to serve the actor.