Candy
Candy is a specification language for stateful backends. You describe a system as actors with state, flows that compose actors, controllers that expose flows over HTTP, policies that capture rules, and events that propagate. From one spec, an LLM generates idiomatic backends in Go, Rust, TypeScript or Python. Files use the .candy extension.
The screenshot above is auth.candy, one of the example specs in the repo, sitting beside the syntax tree our tree-sitter grammar parses out of it: the same file read once by a person and once by a machine.
It is an experiment out of TensorKit, and it is the most speculative thing we have running.
Why we are trying it
Backend code repeats itself across languages. The same actor with state, the same saga with compensation, the same controller mapping HTTP to a handler. What differs between a Go service and a Rust service is mostly idiom, not intent.
So capture the intent once (the actors, the flows, the invariants, the policies) and leave the idiom to the generator. Prose lives in intent: and examples: fields; structure lives in typed blocks. The bet underneath all of it is a single sentence:
A
.candyfile should be readable by a product person and precise enough for a code generator at the same time.
The five word-axes
The language is small, around 50 single-word keywords: prose-heavy where prose serves it, rigorous where ambiguity costs. Every keyword belongs to one of five families.
| Axis | What it expresses | Examples |
|---|---|---|
ENTITY | things that exist | actor, flow, controller, event, policy, type, enum, target |
ACTION | things that happen | ask, tell, emit, effect, commit, compensate, reject, use |
TIME | when, in what order, for how long | now, then, after, before, until, expire |
CONDITION | under what circumstances | if, else, when, require, invariant, unless, need |
INTENT | why this exists, what good looks like | intent, examples, because |
The smallest useful program is a flow and a controller that exposes it:
Hello, candy
flow Hello(name: string) -> string {
intent: "Greet someone by name."
commit "Hello, ${name}!"
}We have not checked in a generated target for that one, so here is the Go we get for a flow of the same shape, one step deeper: a lookup, a policy, an event. This is ToggleTodo, excerpted from the generated internal/todo/flows.go in the todo example, and the comments are the spec’s own steps carried through into the code.
// ToggleTodo flips the done flag on a todo.
// Governed by CanEditTodo.
func ToggleTodo(ctx context.Context, deps Deps, callerID shared.Id, callerRole shared.Role, todoID shared.Id, now time.Time, key shared.Key) (ToggleTodoResult, error) {
// step t = ask Todo.findBy(id: todo)
t, err := deps.Todos.FindByID(ctx, todoID)
if err != nil {
return ToggleTodoResult{}, shared.ErrTodoNotFound
}
// policy: CanEditTodo
if err := CanEditTodo(callerID, callerRole, t); err != nil {
return ToggleTodoResult{}, err
}
// step _ = ask Todo(todo).Toggle(now)
newDone, err := deps.Todos.Toggle(ctx, todoID, now)
if err != nil {
return ToggleTodoResult{}, err
}
// emit TodoToggled
deps.Bus.Publish(ctx, TodoToggled{Todo: todoID, Owner: t.Owner, Done: newDone, At: now})
return ToggleTodoResult{Done: newDone}, nil
}Every great product starts with someone who refuses to accept what exists today.
If that's you, we should
talk about your future product