Tardie, a curious tardigrade.

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.

Five immutable events in the agent log.01MessageReceivedWhat is the meaning of life?02ModelCalledAsk the calculator.03ToolCalledcalculator(7 × 6)04ToolReturned4205ModelCalledConsider the answer.06ModelReturnedThe answer is 42.Log
eventeffectƒ(event, state)Atom
A globe representing the systems and people outside the actor.World
event

Why Tardigrade?

DurableState is derived purely from an immutable event log. Actors survive crashes and resume automatically from where they stopped.
ComposableBuild complex actors from very simple primitives. Actors are made of atoms and they infinitely compose with each other.
ScalableEach actor runs on a single durable object. Scale from one to millions of actors seamlessly.
Native observabilityWith 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@latest

Load the quickstart template. This initialises a simple counter actor along with a SKILL.md for your agent.

sh
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.

TypeScript
TypeScript
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.

TypeScript
TypeScript
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.

TypeScript
TypeScript
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.

TypeScript
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.

server.ts
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),
})
sh
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.

worker.ts
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
json
{
  "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:

sh
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:

sh
bun add -g esbuild
celld deploy . --bucket s3://YOUR-BUCKET

Start your Celld nodes against the same bucket to serve the actor.

Explore