ankka¶
ankka is a serverless application platform for agentic AI, built on the actor model: Scala 3 on Apache Pekko, Apache 2.0 throughout, so the programming model carries no licence constraint on who runs it. You write components; ankka supplies the runtime. Sharding, persistence, replay, projections, durable orchestration, timers, HTTP and the agent loop are the platform's problem, not yours.
ankka is for developers who build services that hold state, react to change, run long processes and
talk to language models, and who want to deploy and operate them without assembling that machinery
themselves. A service is written in Scala, Java or Kotlin in the runtime's own JVM, in Python, TypeScript or Go with
the runtime running beside it as a sidecar, or in Rust built to a WebAssembly module the runtime loads, and is deployed to a Kubernetes cluster
with the ankka command-line tool.
What a service is made of¶
A service is a set of components, registered explicitly and hosted by the runtime:
| Component | What it is for | Scala | Java | Kotlin | Python | TypeScript | Go | Rust |
|---|---|---|---|---|---|---|---|---|
| Event sourced entity | State derived by replaying the events it persisted | yes | yes | yes | yes | yes | yes | yes |
| Key value entity | The latest value only, with no history | yes | yes | yes | yes | yes | yes | yes |
| View | A queryable projection of another component's changes | yes | yes | yes | yes | yes | yes | yes |
| Consumer | Reacting to changes, and optionally publishing onward | yes | yes | yes | yes | yes | yes | yes |
| Workflow | A durable multi-step process that survives restarts | yes | yes | yes | yes | yes | yes | yes |
| Timed action | A call the runtime makes later, on your behalf | yes | yes | yes | yes | yes | yes | yes |
| Agent | A task carried out by talking to a model, with tools and memory | yes | not yet | not yet | yes | yes | yes | yes |
| HTTP endpoint | The service's edge: routes, access control, request handling | yes | yes | yes | yes | yes | yes | yes |
Components describes each path in the picture.
A handler returns an effect, which is a description of what should happen. It performs no I/O itself:
final class ShoppingCartEntity(context: EventSourcedEntityContext)
extends EventSourcedEntity[ShoppingCart, ShoppingCartEvent]:
def emptyState: ShoppingCart = ShoppingCart.empty(context.entityId)
def applyEvent(event: ShoppingCartEvent): ShoppingCart = event match
case ItemAdded(item) => currentState.addItem(item)
case ItemRemoved(productId) => currentState.removeItem(productId)
case CheckedOut => currentState.onCheckedOut
def addItem(item: LineItem): Effect[Done] =
if currentState.checkedOut then effects.error("cart is already checked out", ErrorCode.Conflict)
else effects.persist(ItemAdded(item)).thenReply(_ => Done)
class ShoppingCartEntity(EventSourcedEntity[ShoppingCart, ShoppingCartEvent]):
component_id = "shopping-cart"
state_codec = json_codec(ShoppingCart, "shopping-cart")
event_codec = json_codec(ShoppingCartEvent, "shopping-cart-event")
def empty_state(self) -> ShoppingCart:
return ShoppingCart.empty(self.entity_id)
def apply_event(self, state: ShoppingCart, event: ShoppingCartEvent) -> ShoppingCart:
match event:
case ItemAdded(item):
return state.add_item(item)
case ItemRemoved(product_id):
return state.remove_item(product_id)
case CheckedOut():
return state.on_checked_out()
raise AssertionError(event)
@command("add-item")
def add_item(self, item: LineItem) -> EventSourcedEffect[ShoppingCart, ShoppingCartEvent, Done]:
if self.state.checkedOut:
return self.effects.error("cart is already checked out", ErrorCode.CONFLICT)
return self.effects.persist(ItemAdded(item)).then_reply(lambda _: DONE)
export class ShoppingCartEntity extends EventSourcedEntity<ShoppingCart, ShoppingCartEvent> {
static readonly componentId = "shopping-cart"
static readonly state = jsonCodec(ShoppingCart, "shopping-cart")
static readonly events = jsonCodec(ShoppingCartEvent, "shopping-cart-event")
static readonly handlers = {
addItem: command("add-item", LineItem, Done, (cart: ShoppingCartEntity, item) => cart.addItem(item)),
}
emptyState(): ShoppingCart {
return emptyCart(this.entityId)
}
applyEvent(cart: ShoppingCart, event: ShoppingCartEvent): ShoppingCart {
switch (event.type) {
case "ItemAdded":
return addItem(cart, event.item)
case "ItemRemoved":
return removeItem(cart, event.productId)
case "CheckedOut":
return { ...cart, checkedOut: true }
}
}
addItem(item: LineItem) {
if (this.state.checkedOut) return this.effects.error("cart is already checked out", ErrorCode.Conflict)
return this.effects.persist({ type: "ItemAdded", item }).thenReply(() => done)
}
}
pub struct ShoppingCart;
impl ShoppingCart {
fn add_item(cart: &Cart, item: LineItem, _: &Context) -> Effect<ShoppingCartEvent, Done> {
if cart.checked_out {
return effects::error(ErrorCode::Conflict, "cart is already checked out").into();
}
effects::persist(ShoppingCartEvent::ItemAdded { item }).then_reply_value(Done)
}
}
impl EventSourcedEntity for ShoppingCart {
type State = Cart;
type Event = ShoppingCartEvent;
const COMPONENT_ID: &'static str = "shopping-cart";
const STATE_MANIFEST: Option<&'static str> = Some("shopping-cart");
const EVENT_MANIFEST: Option<&'static str> = Some("shopping-cart-event");
fn empty_state(cart_id: &str) -> Cart {
Cart::empty(cart_id)
}
fn apply(cart: Cart, event: &ShoppingCartEvent) -> Cart {
match event {
ShoppingCartEvent::ItemAdded { item } => cart.add_item(item.clone()),
ShoppingCartEvent::ItemRemoved { product_id } => cart.remove_item(product_id),
ShoppingCartEvent::CheckedOut => cart.on_checked_out(),
}
}
fn handlers() -> Handlers<ShoppingCart> {
Handlers::new().command("add-item", ShoppingCart::add_item)
}
}
/** A customer's cart, one per cart id. Its handlers' wire names are the Scala cart's. */
@ComponentId("shopping-cart")
public class ShoppingCart extends EventSourcedEntity<Cart, CartEvent> {
@Override
protected Cart emptyState() {
return Cart.empty(context().entityId());
}
@Command("add-item")
public Effect<Done> addItem(LineItem item) {
if (currentState().checkedOut()) return effects().error(checkedOut());
if (item.quantity() <= 0)
return effects().error("quantity must be greater than zero, was " + item.quantity());
return effects().persist(new CartEvent.ItemAdded(item)).thenDone();
}
@Command("remove-item")
public Effect<Done> removeItem(String productId) {
if (currentState().checkedOut()) return effects().error(checkedOut());
if (!currentState().contains(productId))
return effects().error(Problem.notFound("cart does not contain '" + productId + "'"));
return effects().persist(new CartEvent.ItemRemoved(productId)).thenDone();
}
@Command("checkout")
public Effect<Cart> checkout() {
if (currentState().checkedOut()) return effects().error(checkedOut());
if (currentState().isEmpty()) return effects().error("cannot check out an empty cart");
return effects().persist(new CartEvent.CheckedOut()).thenReplyState();
}
@Command("discard")
public Effect<Done> discard() {
if (currentState().checkedOut()) return effects().error(checkedOut());
return effects().persist(new CartEvent.Discarded()).thenDelete().thenDone();
}
@Query("get-cart")
public ReadOnlyEffect<Cart> getCart() {
return effects().reply(currentState());
}
@Query("total-quantity")
public ReadOnlyEffect<Integer> totalQuantity() {
return effects().reply(currentState().totalQuantity());
}
@Override
protected Cart applyEvent(Cart state, CartEvent event) {
return switch (event) {
case CartEvent.ItemAdded added -> state.addItem(added.item());
case CartEvent.ItemRemoved removed -> state.removeItem(removed.productId());
case CartEvent.CheckedOut ignored -> state.onCheckedOut();
case CartEvent.Discarded ignored -> state;
};
}
private static Problem checkedOut() {
return Problem.conflict("cart is already checked out");
}
}
/** A customer's cart, one per cart id. Its handlers' wire names are the Scala cart's. */
@ComponentId("shopping-cart")
class ShoppingCart : EventSourcedEntity<Cart, CartEvent>() {
override fun emptyState() = Cart.empty(context().entityId())
// A suspend fun, as any handler may be: it waits without holding a thread.
@Command("add-item")
suspend fun addItem(item: LineItem): Effect<Done> = effects {
when {
currentState().checkedOut -> error(checkedOut)
item.quantity <= 0 -> error("quantity must be greater than zero, was ${item.quantity}")
else -> persist(CartEvent.ItemAdded(item)).thenDone()
}
}
@Command("remove-item")
fun removeItem(productId: String): Effect<Done> = effects {
when {
currentState().checkedOut -> error(checkedOut)
!currentState().contains(productId) -> error(Problem.notFound("cart does not contain '$productId'"))
else -> persist(CartEvent.ItemRemoved(productId)).thenDone()
}
}
@Command("checkout")
fun checkout(): Effect<Cart> = effects {
when {
currentState().checkedOut -> error(checkedOut)
currentState().items.isEmpty() -> error("cannot check out an empty cart")
else -> persist(CartEvent.CheckedOut).thenReplyState()
}
}
@Command("discard")
fun discard(): Effect<Done> = effects {
if (currentState().checkedOut) error(checkedOut) else persist(CartEvent.Discarded).thenDelete().thenDone()
}
@Query("get-cart")
fun getCart(): ReadOnlyEffect<Cart> = effects { reply(currentState()) }
@Query("total-quantity")
fun totalQuantity(): ReadOnlyEffect<Int> = effects { reply(currentState().totalQuantity) }
override fun applyEvent(state: Cart, event: CartEvent): Cart =
when (event) {
is CartEvent.ItemAdded -> state.addItem(event.item)
is CartEvent.ItemRemoved -> state.removeItem(event.productId)
CartEvent.CheckedOut -> state.copy(checkedOut = true)
CartEvent.Discarded -> state
}
private val checkedOut: Problem
get() = Problem.conflict("cart is already checked out")
}
The runtime interprets the effect: it writes the event to the journal, applies it to the state and replies. Because building an effect does nothing, a component's decisions can be tested with nothing running. Effects are data explains the idea in full.
Where to start¶
- Building a feature. Design, then build: describe it, answer the design's questions, and approve the pull request the build raises.
- New to ankka. Install the tools, then build your first service in Scala, in Java, in Kotlin, in Python, in TypeScript, in Go or in Rust, and deploy it to a local platform.
- Designing a system. How ankka works is the overview, and Designing a service maps requirements onto components.
- Building components. The build guides start with event sourced entities, and each shows Scala and Python side by side.
- Deploying. Deploy a service covers the descriptor and the CLI; Expose a service puts it on the internet.
- Operating. The local console shows traces on your own machine, and Troubleshooting collects the problems people hit.
- Running the platform. Install a local platform sets up a kind cluster with everything a service needs.
- Looking something up. The CLI reference, service descriptor and glossary.
- Checking what is missing. Limitations lists what ankka does not do yet.
For models and agents¶
This documentation is published in forms a model can read directly. llms.txt at the site root lists
every page with a one-sentence description, llms-full.txt holds every page in one file, and each
page is also served as Markdown at its own path with a .md suffix. The same pages ship as agent
skills, one per kind of task: in the Claude Code plugin published to the ankka marketplace, and inside
every project created from the service template. ankka mcp serves the platform to an agent as MCP tools: the CLI's service commands,
the services running on your machine, and this documentation.
Work with a coding agent sets both up.