Examples

Three shapes that come up constantly. Each one is complete enough to paste into a project and adjust.

On this page

The samples below leave out the helper functions they call (createOrder, download and so on) — the point is the shape of the flow.

An event racing a timeout

Close the order unless it's paid within thirty minutes. The hard part was never the timer — it's that payment and timeout collide, and two flows canceling each other is a race you have to win yourself. Here the race is settled by one atomic operation, so a payment landing at 29:59.9 is either accepted or rejected, never both.

tick.Flow("order/close", func(ctx *gotick.Context) {    orderId, _ := ctx.MetaData("order_id")     gotick.Task(ctx, "create-order", func(c *gotick.TaskContext) error {        return orders.Create(c, orderId)    })     paid, ok := gotick.WaitForSignal[Payment](ctx, "paid",        gotick.WithSignalTimeout(30*time.Minute))     if !ok {        gotick.Task(ctx, "close-order", func(c *gotick.TaskContext) error {            return orders.Close(c, orderId)        })        return    }     gotick.Task(ctx, "ship", func(c *gotick.TaskContext) error {        return shipping.Send(c, orderId, paid.TradeNo)    })})

Fan out, then collect

Download a list of files with at most four running at once. Array remembers the list so every run agrees on what to loop over, AsyncArray turns it into parallel steps, and Wait holds until they're done. Each item keeps its own state, so a restart resumes the ones that hadn't finished.

tick.Flow("media/convert", func(ctx *gotick.Context) {    dir, _ := ctx.MetaData("dir")     files := gotick.Array(ctx, "list", func(c *gotick.TaskContext) ([]string, error) {        return storage.List(c, dir)    })     fs := gotick.AsyncArray(ctx, "convert", files,        func(c *gotick.TaskContext, name string, i int) (string, error) {            return ffmpeg.Convert(c, name)        })     gotick.Wait(ctx, 4, fs...)     gotick.Task(ctx, "publish", func(c *gotick.TaskContext) error {        out := make([]string, 0, len(fs))        for _, f := range fs {            out = append(out, f.(*gotick.FutureT[string]).Value())        }        return catalog.Publish(c, dir, out)    })})

Only the last edit matters

A user changes the same document ten times in a minute and each change triggers an expensive regeneration — but only the last result will ever be looked at. Keying the flow by document id makes the previous nine cancel themselves, and you stop paying for work nobody reads.

tick.Flow("doc/preview", func(ctx *gotick.Context) {    docId, _ := ctx.MetaData("doc_id")     // expensive, and only the newest result will ever be looked at    url := gotick.Memo(ctx, "render", func() (string, error) {        return renderer.Build(docId)    })     gotick.Task(ctx, "attach", func(c *gotick.TaskContext) error {        return docs.SetPreview(c, docId, url)    })}) // in the save handler — ten edits, nine of them canceled automaticallytick.Trigger(ctx, "doc/preview", gotick.MetaData{"doc_id": docId},    gotick.WithKey(docId))

More runnable examples live in the repo's example directory — including this order-timeout one, which you can run against a real Redis in a few seconds. example/

Render diagnostics