# Driving from outside · gotick docs

> Starting, feeding and stopping a flow happens from your normal code — an HTTP handler, a webhook, a CLI. All of it goes through Redis, so the caller doesn't have to be the process that runs the flow.

Documentation

- [01Install and run](/en/docs.md)
- [02How it works](/en/docs/model.md)
- [03What's underneath](/en/docs/internals.md)
- [04Primitives](/en/docs/primitives.md)
- [05Driving from outside](/en/docs/control.md)
- [06Examples](/en/docs/examples.md)
- [07Running it](/en/docs/operations.md)
- [08Limits](/en/docs/limits.md)

# Driving a flow from outside

Starting, feeding and stopping a flow happens from your normal code — an HTTP handler, a webhook, a CLI. All of it goes through Redis, so the caller doesn't have to be the process that runs the flow.

On this page

- [Trigger](#trigger)
- [WithKey](#key)
- [Supersede](#supersede)
- [SendSignal](#signal)
- [Cancel](#cancel)

## Trigger

`func (t *Server) Trigger(ctx context.Context, flowId string, data MetaData, opts ...TriggerOption) (string, error)`

Start a flow and get back a callId. The MetaData map is the flow's input; inside the flow you read it with ctx.MetaData(k). WithDelay(d) starts it later instead of now.

Copy

```
callId, err := tick.Trigger(ctx, "order/close", gotick.MetaData{    "order_id": order.Id,    "user_id":  order.UserId,}, gotick.WithDelay(5*time.Second))
```

## WithKey

`func WithKey(key string) TriggerOption`

Name the call with an identifier you already have — an order number, a user id — and address it by that afterwards, instead of storing gotick's callId in a column that means nothing to your business. The scope is (flow, key), so two different flows using the same order number don't collide.

Copy

```
_, err := tick.Trigger(ctx, "order/close", meta, gotick.WithKey(order.Id)) // later, from anywhere, with no callId storederr = tick.CancelByKey(ctx, "order/close", order.Id, "user canceled")
```

## Supersede

Trigger the same (flow, key) again and the previous unfinished call is canceled automatically; only the latest one carries on. The old call lands in a canceled state naming what replaced it — it isn't deleted, so you can still see it in the inspector. The binding is released the moment a call reaches a terminal state, so the same key is free to run again later. Note this is supersede, not dedup: there is no "skip if one is already running" mode.

Copy

```
// each edit supersedes the unfinished run from the edit before itfor _, edit := range edits {    tick.Trigger(ctx, "doc/render", meta, gotick.WithKey(edit.DocId))}// only the last one reaches its final step
```

## SendSignal

`func (t *Server) SendSignal(ctx context.Context, callId, key string, value any) (bool, error)func (t *Server) SendSignalByKey(ctx context.Context, flowId, key, signal string, value any) (bool, error)`

Hand a waiting flow the thing it's waiting for. The returned bool is whether the signal was accepted — false means that slot was already settled, either by an earlier signal or by the timeout. Treat false as information, not an error: it is exactly how you learn you lost the race.

Copy

```
// in your payment webhook handleraccepted, err := tick.SendSignalByKey(ctx, "order/close", orderId, "paid", Payment{    TradeNo: notify.TradeNo,    Amount:  notify.Amount,})if err != nil {    return err}if !accepted {    // the timeout already won: the order is closed, so refund instead    return refund(ctx, notify.TradeNo)}
```

## Cancel

`func (t *Server) Cancel(ctx context.Context, callId, reason string) errorfunc (t *Server) CancelByKey(ctx context.Context, flowId, key, reason string) error`

Stop a call wherever it currently is. Three things happen: the flag is checked unconditionally at the top of every round, a sleeping or signal-waiting flow is woken immediately rather than left to its timer, and the context handed to a running Task is canceled so your own code can return early — that last one is polled every 3 seconds. Canceling an unknown call returns ErrRunNotFound, and one that already finished returns ErrRunNotCancelable, so a late cancel can never rewrite a completed result.

Copy

```
err := tick.Cancel(ctx, callId, "user canceled") switch {case errors.Is(err, gotick.ErrRunNotFound):    // never existed, or already cleaned upcase errors.Is(err, gotick.ErrRunNotCancelable):    // already finished — nothing was rewritten}
```

A note on Client: it only has Trigger(ctx, flowId, data, delay) — no options, so no WithKey, and no SendSignal or Cancel. If a non-worker process needs those, build a Server with NewServerFromConfig and simply never call StartServer. Triggering, signalling and cancelling all go through Redis and work fine without the scheduler running.

[← Previous\
\
Primitives](/en/docs/primitives.md) [Next →\
\
Examples](/en/docs/examples.md)

On this page

- [Trigger](#trigger)
- [WithKey](#key)
- [Supersede](#supersede)
- [SendSignal](#signal)
- [Cancel](#cancel)

> Full page index: [/llms.txt](/llms.txt)
