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.Keyto 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.
Openlocks the file, so a second process getsErrLocked.
The reference lists every call and status, and the contract has the exact rules.