Quick Start

This page takes you from nothing to a flow that survives being killed. You need Go 1.24 or later.

1. Add the Module

mkdir orders && cd orders
go mod init example.com/orders
go get github.com/flowchassis/flowchassis@latest

2. Write a Flow

A flow is a name and a list of steps. Each step has a Do, and can have an Undo that reverses it. Save this as main.go:

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/flowchassis/flowchassis"
)

func main() {
	e, err := flowchassis.Open("orders.journal", flowchassis.Options{})
	if err != nil {
		log.Fatal(err)
	}
	defer e.Close()

	slow := func(c *flowchassis.Ctx) error {
		time.Sleep(2 * time.Second) // time to press Ctrl-C
		return nil
	}

	err = e.Register(flowchassis.Flow{Name: "order", Steps: []flowchassis.Step{
		{
			Name: "reserve",
			Do: func(c *flowchassis.Ctx) error {
				fmt.Println("reserve stock, attempt", c.Attempt)
				c.Set("reservation", []byte("R-1001"))
				return slow(c)
			},
			Undo: func(c *flowchassis.Ctx) error {
				r, _ := c.Get("reservation")
				fmt.Println("release reservation", string(r))
				return nil
			},
		},
		{
			Name: "charge",
			Do: func(c *flowchassis.Ctx) error {
				fmt.Println("charge card with idempotency key", c.Key, "attempt", c.Attempt)
				return slow(c)
			},
			Undo: func(c *flowchassis.Ctx) error {
				fmt.Println("refund with idempotency key", c.Key)
				return nil
			},
		},
		{
			Name: "ship",
			Do: func(c *flowchassis.Ctx) error {
				if len(os.Args) > 1 && os.Args[1] == "refuse" {
					return flowchassis.Permanent(errors.New("carrier refused the address"))
				}
				fmt.Println("ship")
				return slow(c)
			},
		},
	}})
	if err != nil {
		log.Fatal(err)
	}

	id, err := e.Start("order", "order-1001", []byte(`{"sku":"A-42"}`))
	if err != nil {
		log.Fatal(err)
	}
	run, err := e.Wait(context.Background(), id)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("order-1001 is", run.Status)
}

3. Run It, and Kill It

go run .

While it prints charge card, press Ctrl-C. Then run it again:

go run .

The second run doesn’t reserve stock again. Start finds order-1001 already in the journal and returns it, the engine sees that reserve is done and charge was under way, and it runs charge again as attempt 2, with the same idempotency key. Then ship, and the order is completed.

The same thing happens with kill -9, a power cut, or a crash in your own code. That is the main promise: the journal says where every run is, and the engine carries on from there.

4. Watch It Compensate

Delete the journal and run the flow with shipping refused:

rm orders.journal
go run . refuse

ship fails with a permanent error, so the run compensates: the refund runs first, then the reservation is released, newest first, and the order ends compensated. A step whose error isn’t marked permanent is retried first, three attempts by default.

5. Look Inside the Journal

Install the command-line tool and point it at the journal:

flowchassis runs orders.journal
flowchassis show orders.journal order-1001
flowchassis check orders.journal

show prints every record of the run: each attempt, each failure, each undo, with times. These commands don’t lock the journal, so they work while your program is running.

What to Know Next

  • Steps run at least once. A step interrupted by a crash runs again. Pass c.Key to payment providers, couriers and other outside services as an idempotency key.
  • Keep step names stable. Unfinished runs resume by name.
  • Keep state small. IDs, references and amounts. Each step’s state is written whole with its checkpoint.
  • One process per journal. Open locks the file, so a second process gets ErrLocked.

The reference lists every call and status, and the contract has the exact rules.