Skip to content

dandori

Write the workflow. Check it. Build it.

A small typed language for workflows that call business rules. A workflow books a hotel stay, reserves the lines of an order, answers a customer's inquiry: it calls APIs and rules, waits, retries, and drives things like a Stripe PaymentIntent from state to state.

Checked before it runs. Types, every arm of every match, every state a payment or an order can be left in when the workflow ends, retries that could repeat a change on the other side, the service of a `.proto` the workflow implements, and how long a run's history can grow on the platform it is built for.

Built for five platforms. Temporal (TypeScript, Python or Go), AWS Step Functions, AWS Lambda durable functions, Argo Workflows and pydantic-graph. What each of them runs is played against one reference interpreter, on every scenario the tests generate.

A .flow and the rulec rules it calls go into the checker, which looks at the types and every arm of every match, every state a case can be left in, retries that could repeat a change, the history's size on each platform, ranges, child .flows and API descriptions, and what each platform can do. The reference interpreter gives the one meaning, and scenarios take every arm and every error. The build writes for Temporal (the main platform), AWS Step Functions, Lambda durable functions, Argo Workflows and pydantic-graph, and each is run on every scenario and held to the reference A .flow and the rulec rules it calls go into the checker, which looks at the types and every arm of every match, every state a case can be left in, retries that could repeat a change, the history's size on each platform, ranges, child .flows and API descriptions, and what each platform can do. The reference interpreter gives the one meaning, and scenarios take every arm and every error. The build writes for Temporal (the main platform), AWS Step Functions, Lambda durable functions, Argo Workflows and pydantic-graph, and each is run on every scenario and held to the reference


What dandori does

case pi : PaymentIntent follows payment_intent.payment
  held capture_method = manual
  held confirmation_method = automatic
  external authenticate, settle, expire
  refused when refused = true

flow
  let quote = hold(room: booking.room, nights: booking.nights)
  match quote.handling
    review => succeed outcome = awaiting_review
    auto => pi <- create_intent(amount: quote.amount, …)
  pi <- confirm_intent(intent: pi.id)
    on card_declined => pi <- get_intent(intent: pi.id)

Decisions come from outside the workflow

A .flow has no comparison and no arithmetic of its own. It branches only by matching an enum, a bool, or a value that may be absent, which a rule or a task answered: an API, an agent, Jev, your own code, a person's approval. A decision that must have no gaps can be written in rulec, as a table that rulec proves complete and free of overlaps, and a rule's state machine becomes the type of what the workflow drives, here Stripe's PaymentIntent.

error[E020]: tests/fixtures/hotel_naive.flow:95:1: the workflow can end here with the case `pi` in requires_payment_method, processing, which is not final (succeeded, canceled are)
    95 |   succeed outcome = stayed
  the run that gets there:
      81  quote = hold(…)
      84  match quote.handling: auto
      84  create_intent: pi starts in requires_confirmation
      85  confirm_intent: pi requires_confirmation → requires_capture
      90  match pi.status: requires_capture
      90  wait until booking.check_out
      93  capture_intent: pi requires_capture → processing
          `settle` happens on the other side: pi processing → requires_payment_method
      95  succeed

The checker follows every way it can go

A first draft of the hotel booking waits until check-out and then captures the payment. The checker follows the transitions of Stripe's PaymentIntent, including the ones that happen on Stripe's side without the workflow's asking, and finds a run that ends with the payment neither settled nor released. Each diagnostic comes with the run that gets there. What it checks

dandori build hotel.flow --target temporal
dandori build hotel.flow --target temporal-python
dandori build hotel.flow --target temporal-go
dandori build hotel.flow --target asl
dandori build hotel.flow --target durable
dandori build hotel.flow --target argo
dandori build hotel.flow --target pydantic-graph

One workflow, built for the platform you run

Temporal is the main platform: dandori writes the workflow, the activities that make its HTTP, AWS and agent calls, the worker and the client, in TypeScript, Python or Go. The same .flow also builds for AWS Step Functions, Lambda durable functions, Argo Workflows and pydantic-graph, and the code dandori writes sends the same requests on each. A build refuses what its platform cannot do. Build for a platform

task read_inquiry(text: string) -> Reading
  agent "Read the text of a customer's inquiry, choose its kind, …"
  model "gpt-oss:20b"
  effort low
  url "http://ollama.internal:11434/v1"
  plaintext "The model server is reached only inside the cluster network, which the service mesh encrypts"
  timeout 60 seconds
  retry 2 times every 10 seconds

Agents read, write and choose

A task can be an agent: a model that gets the task's arguments and gives back a value of the task's type, checked like any other answer. The flow can match that answer, or hand it to a rule to decide. OpenAI's models, Claude, and any Open Responses endpoint (Ollama, vLLM, LM Studio, OpenRouter, …) can be called. Agents

task pick_kind(text: string) -> routing.kind
  jev "Which kind of inquiry is this?"
    returns "The customer wants to send an item back or exchange it"
    delivery "A parcel is late, lost or damaged, or the customer asks where it is"
    …
  model "jev-1.13.0"
  confidence 0.8 else unsure

Jev decides, and says how sure

A task can ask TypeSafe's Jev, which writes no text: it answers typed questions, each with how sure it is. The task's answer type is the question, a choice among an enum's values, a place on a scale of them, or yes or no, and an answer less sure than the task asks fails the call with an error the flow handles. How sure is enough for what can be a rule's table. Jev

task reserve(order: string, sku: string, qty: int) -> stock.reserve
  book stock.reserve.hold
  starts stock.reserve
  errors out_of_stock
…
  let due = terms.payment(received: now)
  wait until due.at

Due dates counted, stock held

A due date can be a date of koyomi's, which counts business days and is checked on every day of its range, and stock can be a book of chobo's, whose bounds hold in every write. A workflow calls a date as it calls a rule, and holds, posts and voids stock as tasks. A hold is a case, and the checker counts that it may expire before it is posted. Dates and books

dandori doc hotel.flow > hotel.md
dandori doc hotel.flow --format html > hotel.html

Drawn for the person who reviews it

dandori doc draws a workflow: every call, match, wait and loop, with what each call does, where its errors go, what a case can be after it, and every way the workflow can end. As Markdown, it is a Mermaid flowchart that GitHub draws in a pull request; as one HTML page, each scenario lights up the way its run goes. The hotel booking, drawn · Draw a workflow

Status

Early. Not yet: Parallel with different branches, OpenAPI documents in YAML, types made from an OpenAPI document or a Smithy model (a .proto makes them), protobuf's binary encoding and Connect's streams, the clients of a service a workflow implements written for other languages by a plugin of protoc, cases the workflow holds itself, a rule's preconditions checked at the task that produced the value, runs on AWS and on a production Temporal cluster or Temporal Cloud, the caller image run against real Lambda, HTTP and AWS endpoints from Argo, and agents run against OpenAI and Anthropic themselves. The design, the decisions and what is left are in DESIGN.md, in Japanese; its principles are on Design. dandori is licensed under either of the Apache License 2.0 or the MIT license, at your option.