# Durable Workflows in Go

> Create functions that run steps in the background, triggered by events or schedules.

A [durable workflow](/docs-markdown/durable-execution/durable-workflows) is an Inngest function that runs in the background. An event or a schedule starts a run, and each step saves its result so that a retry or a wait doesn't repeat finished work. In Go, you create one with `inngestgo.CreateFunction`, then [serve it over HTTP or connect a worker](#serve-or-connect-your-functions) so that Inngest can run it.

## Create a function

`CreateFunction` takes the client, the function's options, a trigger, and a handler:

```go {{ title: "process_order.go" }}
import (
	"context"
	"errors"
	"time"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/step"
)

type OrderPlaced struct {
	OrderID string `json:"orderId"`
}

type OrderCancelled struct {
	OrderID string `json:"orderId"`
}

func ProcessOrder(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "process-order"},
		// any time the `shop/order.placed` event is received, this will run.
		inngestgo.EventTrigger("shop/order.placed", nil),
		func(ctx context.Context, input inngestgo.Input[OrderPlaced]) (any, error) {
			order, err := step.Run(ctx, "load-order", func(ctx context.Context) (Order, error) {
				return loadOrder(ctx, input.Event.Data.OrderID)
			})
			if err != nil {
				return nil, err
			}
			if _, err := step.Run(ctx, "charge-order", func(ctx context.Context) (any, error) {
				return nil, chargeOrder(ctx, order)
			}); err != nil {
				return nil, err
			}
			if _, err := step.Run(ctx, "send-receipt", func(ctx context.Context) (any, error) {
				return nil, sendReceipt(ctx, order)
			}); err != nil {
				return nil, err
			}

			// this will wait for the `shop/order.cancelled` event for up to 6 hours,
			// and resume immediately when a matching event is received.  If an event
			// isn't received within 6 hours, the function resumes and WaitForEvent
			// returns step.ErrEventNotReceived.
			//
			// your compute is *not running* during this wait, and you do not need to handle
			// any matching.
			_, err = step.WaitForEvent[inngestgo.GenericEvent[OrderCancelled]](
				ctx,
				"wait-for-cancellation",
				step.WaitForEventOpts{
					Event:   "shop/order.cancelled",
					If:      inngestgo.StrPtr("event.data.orderId == async.data.orderId"),
					Timeout: 6 * time.Hour,
				},
			)
			if errors.Is(err, step.ErrEventNotReceived) {
				return map[string]any{"orderId": order.ID}, nil
			}
			if err != nil {
				return nil, err
			}

			// the order was cancelled, as we received a cancel event
			if _, err := step.Run(ctx, "refund-order", func(ctx context.Context) (any, error) {
				return nil, refundOrder(ctx, order)
			}); err != nil {
				return nil, err
			}
			return map[string]any{"orderId": order.ID, "status": "cancelled"}, nil
		},
	)
}
```

`FunctionOpts.ID` is required and must stay the same across deploys, because Inngest uses it to find the function's runs. `CreateFunction` registers the function with the client, so `client.Serve()` serves it with no extra step.

### The handler

The handler has the signature `func(ctx context.Context, input inngestgo.Input[T]) (any, error)`. `T` is the type of the event's `data` field, and the SDK decodes the event into it:

| Field            | Description                                                                                                                                     |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `input.Event`    | The event that started the run. `input.Event.Data` has type `T`.                                                                                |
| `input.Events`   | Every event in the run. This has more than one event when the function uses [batching](/docs-markdown/durable-execution/flow-control/batching). |
| `input.InputCtx` | The run's context, such as `RunID`, `FunctionID`, and `Attempt`.                                                                                |

Use `any` or `map[string]any` as `T` when the function doesn't read event data, for example a cron function. The value the handler returns is the run's output, and an error fails the current attempt.

Always pass the `ctx` that the handler receives to step tools. Steps use it to find the run's saved state.

## Serve or connect your functions

Inngest runs your functions in one of two ways. Both use the same client and functions, so you can switch later without changing your function code.

| Option                                                                  | How it works                                                            | Use it when                                                    |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------- |
| [Serve](/docs-markdown/durable-execution/deploying-functions/serve)     | Inngest sends an HTTP request to an endpoint in your app for each step. | Your app runs an HTTP server, including serverless platforms.  |
| [Connect](/docs-markdown/durable-execution/deploying-functions/connect) | Your worker opens a WebSocket to Inngest and receives work over it.     | You run long-lived workers, or can't expose a public endpoint. |

### Serve over HTTP

Mount `client.Serve()` on any `net/http` server, usually at `/api/inngest`:

```go {{ title: "main.go" }}
import (
	"log"
	"net/http"

	"github.com/inngest/inngestgo"
)

func Serve() {
	client, err := inngestgo.NewClient(inngestgo.ClientOpts{AppID: "billing"})
	if err != nil {
		log.Fatal(err)
	}

	// Create your functions with inngestgo.CreateFunction(client, ...) before
	// serving.  Each one registers itself with the client.

	// Inngest calls this endpoint to run your functions.
	http.Handle("/api/inngest", client.Serve())
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

In production, set `INNGEST_SIGNING_KEY` so the handler can verify requests from Inngest, then [sync your app](/docs-markdown/platform-and-operations/apps-and-syncs) with its URL. Use `client.ServeWithOpts` to change options such as the path that Inngest calls.

### Connect a worker

Call `inngestgo.Connect` with the clients to run. It returns once the connection is active:

```go {{ title: "worker.go" }}
import (
	"context"
	"log"
	"os/signal"
	"syscall"

	"github.com/inngest/inngestgo"
)

func Worker() {
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	client, err := inngestgo.NewClient(inngestgo.ClientOpts{AppID: "billing"})
	if err != nil {
		log.Fatal(err)
	}

	// Create your functions with inngestgo.CreateFunction(client, ...) before
	// connecting.  Each one registers itself with the client.

	// Connect opens a WebSocket to Inngest and returns once it's active.
	conn, err := inngestgo.Connect(ctx, inngestgo.ConnectOpts{
		Apps: []inngestgo.Client{client},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Close finishes in-flight steps before it returns.
	<-ctx.Done()
	if err := conn.Close(); err != nil {
		log.Fatal(err)
	}
}
```

A Connect worker syncs itself when it connects, so you don't sync it by URL. Set `INNGEST_SIGNING_KEY` in production. See [Connect](/docs-markdown/durable-execution/deploying-functions/connect) for instance IDs, worker concurrency, and health checks.

For local development, set `INNGEST_DEV=1` and run the [Dev Server](/docs-markdown/local-development) with either option.

## Triggers

| Trigger                                         | Starts a run                                                                                                                                                                                                                         |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `inngestgo.EventTrigger(name, expression)`      | For each event named `name`. Pass a [CEL expression](/docs-markdown/durable-execution/guides-and-advanced/writing-expressions), such as `inngestgo.StrPtr("event.data.plan != 'free'")`, to filter events, or `nil` for every event. |
| `inngestgo.CronTrigger(cron)`                   | On a [schedule](/docs-markdown/durable-execution/guides-and-advanced/events-and-triggers/schedules-and-delayed-starts). Prefix the expression with `TZ=` to set a time zone.                                                         |
| `inngestgo.CronTriggerWithJitter(cron, jitter)` | On a schedule, with each run delayed by a random amount up to `jitter`.                                                                                                                                                              |
| `inngestgo.MultipleTriggers{...}`               | For any of the triggers in the list.                                                                                                                                                                                                 |

```go {{ title: "sync_account.go" }}
import (
	"context"
	"time"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/step"
)

type AccountUpdated struct {
	AccountID string `json:"accountId"`
	Plan      string `json:"plan"`
}

func SyncAccount(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "sync-account"},
		inngestgo.MultipleTriggers{
			// Run for each account update on a paid plan.
			inngestgo.EventTrigger("account/updated", inngestgo.StrPtr("event.data.plan != 'free'")),
			// Also run every night, spread across up to 10 minutes.
			inngestgo.CronTriggerWithJitter("TZ=America/New_York 0 2 * * *", 10*time.Minute),
		},
		func(ctx context.Context, input inngestgo.Input[AccountUpdated]) (any, error) {
			// A cron run has no event data, so AccountID is empty.
			_, err := step.Run(ctx, "sync-account", func(ctx context.Context) (any, error) {
				return nil, syncAccount(ctx, input.Event.Data.AccountID)
			})
			return nil, err
		},
	)
}
```

Send events that trigger functions with `client.Send` or `client.SendMany`. See [sending events](/docs-markdown/durable-execution/guides-and-advanced/events-and-triggers/send-events).

## Steps

Step tools are in the [`step`](https://pkg.go.dev/github.com/inngest/inngestgo/step) package. Each one takes the handler's `ctx` and a step ID:

| Step tool                                                                                  | Description                                                                                                  |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| [`step.Run`](/docs-markdown/durable-execution/primitives/step-run)                         | Run code, such as an API call or a database write, and save its result. A failed step retries on its own.    |
| [`step.Sleep`](/docs-markdown/durable-execution/primitives/step-sleep)                     | Pause the run for a duration.                                                                                |
| [`step.SleepUntil`](/docs-markdown/durable-execution/primitives/step-sleepuntil)           | Pause the run until a time.                                                                                  |
| [`step.WaitForEvent`](/docs-markdown/durable-execution/primitives/step-waitforevent)       | Pause until a matching event arrives. On timeout, it returns `step.ErrEventNotReceived`.                     |
| [`step.WaitForSignal`](/docs-markdown/durable-execution/primitives/step-waitforsignal)     | Pause until one run receives a unique signal.                                                                |
| [`step.Invoke`](/docs-markdown/durable-execution/primitives/step-invoke)                   | Run another function and wait for its result.                                                                |
| [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent), `step.SendMany` | Send events as a step, so a retry doesn't send them again.                                                   |
| `step.Fetch`, `step.Infer`                                                                 | Make an HTTP request or an AI inference call that Inngest runs for you, so your function doesn't wait on it. |
| [`group.Parallel`](/docs-markdown/durable-execution/primitives/group-parallel)             | Run steps in parallel. It's in the [`group`](https://pkg.go.dev/github.com/inngest/inngestgo/group) package. |

A sleep or wait doesn't keep your process running. Inngest calls your function again when the wait ends, and finished steps return their saved results without running again. Put code with side effects inside a step so that it runs once.

## Errors and retries

When a step or the handler returns an error, Inngest retries it with backoff. By default, each step retries 4 times. Set `FunctionOpts.Retries` to change this. The [`errors`](https://pkg.go.dev/github.com/inngest/inngestgo/errors) package controls retries for one error:

- `errors.NoRetryError(err)` fails the step or run with no more retries.
- `errors.RetryAtError(err, at)` retries at a time that you choose, for example when an API sends a `Retry-After` header.

```go {{ title: "build_report.go" }}
import (
	"context"
	"fmt"
	"time"

	"github.com/inngest/inngestgo"
	inngesterrors "github.com/inngest/inngestgo/errors"
	"github.com/inngest/inngestgo/step"
)

type ReportRequested struct {
	ReportID string `json:"reportId"`
}

var errRateLimited = fmt.Errorf("rate limited")

func BuildReport(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "build-report"},
		inngestgo.EventTrigger("report/requested", nil),
		func(ctx context.Context, input inngestgo.Input[ReportRequested]) (any, error) {
			if input.Event.Data.ReportID == "" {
				// The event can never succeed, so don't retry.
				return nil, inngesterrors.NoRetryError(fmt.Errorf("reportId is required"))
			}

			return step.Run(ctx, "build-report", func(ctx context.Context) (Report, error) {
				report, err := buildReport(ctx)
				if err == errRateLimited {
					// Retry this step in 5 minutes instead of on the default backoff.
					return Report{}, inngesterrors.RetryAtError(err, time.Now().Add(5*time.Minute))
				}
				// Any other error retries the step with backoff.
				return report, err
			})
		},
	)
}
```

When a step runs out of retries, `step.Run` returns the error to the handler. Return it to fail the run, or handle it, for example to [roll back](/docs-markdown/durable-execution/guides-and-advanced/error-handling/rollbacks) earlier steps. See [error handling](/docs-markdown/durable-execution/guides-and-advanced/error-handling) for failure handlers and more.

## Function options

`FunctionOpts` configures retries, flow control, cancellation, and timeouts:

```go {{ title: "charge_invoice.go" }}
import (
	"context"
	"time"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/step"
)

type InvoiceCreated struct {
	InvoiceID string `json:"invoiceId"`
	AccountID string `json:"accountId"`
}

func ChargeInvoice(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{
			ID:   "charge-invoice",
			Name: "Charge invoice",
			// Retry each failed step up to 10 times.
			Retries: inngestgo.IntPtr(10),
			// Run one invoice at a time for each account.
			Concurrency: &inngestgo.ConfigConcurrency{
				Fn: []inngestgo.ConfigFnConcurrency{
					{Limit: 1, Key: inngestgo.StrPtr("event.data.accountId")},
				},
			},
			// Start at most 100 runs a minute.
			Throttle: &inngestgo.ConfigThrottle{Limit: 100, Period: time.Minute},
			// Cancel the run when the invoice is voided.
			Cancel: []inngestgo.ConfigCancel{
				{Event: "invoice/voided", If: inngestgo.StrPtr("event.data.invoiceId == async.data.invoiceId")},
			},
			// Cancel the run if it doesn't finish within an hour.
			Timeouts: &inngestgo.ConfigTimeouts{Finish: inngestgo.Ptr(time.Hour)},
		},
		inngestgo.EventTrigger("invoice/created", nil),
		func(ctx context.Context, input inngestgo.Input[InvoiceCreated]) (any, error) {
			invoice, err := step.Run(ctx, "load-invoice", func(ctx context.Context) (Invoice, error) {
				return loadInvoice(ctx, input.Event.Data.InvoiceID)
			})
			if err != nil {
				return nil, err
			}
			_, err = step.Run(ctx, "charge", func(ctx context.Context) (any, error) {
				return nil, chargeInvoice(ctx, invoice)
			})
			return nil, err
		},
	)
}
```

| Field         | Type                 | Description                                                                                                                                            |
| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ID`          | `string`             | Required. The function ID.                                                                                                                             |
| `Name`        | `string`             | A name for the dashboard.                                                                                                                              |
| `Retries`     | `*int`               | The number of retries for each step. See [retries](/docs-markdown/durable-execution/guides-and-advanced/error-handling/retries).                       |
| `Concurrency` | `*ConfigConcurrency` | Limits on running runs (`Fn`) or steps (`Step`), with an optional key. See [concurrency](/docs-markdown/durable-execution/flow-control/concurrency).   |
| `Throttle`    | `*ConfigThrottle`    | Limit how often runs start, and queue the rest. See [throttling](/docs-markdown/durable-execution/flow-control/throttling).                            |
| `RateLimit`   | `*ConfigRateLimit`   | Limit how often runs start, and skip the rest. See [rate limiting](/docs-markdown/durable-execution/flow-control/rate-limiting).                       |
| `Debounce`    | `*ConfigDebounce`    | Wait for events to stop before a run starts. See [debounce](/docs-markdown/durable-execution/flow-control/debounce).                                   |
| `BatchEvents` | `*ConfigBatchEvents` | Start one run for many events. See [batching](/docs-markdown/durable-execution/flow-control/batching).                                                 |
| `Singleton`   | `*ConfigSingleton`   | Allow one run at a time for each key. See [singleton](/docs-markdown/durable-execution/flow-control/singleton).                                        |
| `Priority`    | `*ConfigPriority`    | Run some runs before others. See [priority](/docs-markdown/durable-execution/flow-control/priority).                                                   |
| `Idempotency` | `*string`            | A CEL expression. Only one run starts for each value in 24 hours. See [idempotency](/docs-markdown/durable-execution/guides-and-advanced/idempotency). |
| `Cancel`      | `[]ConfigCancel`     | Events that cancel a run. See [cancel on events](/docs-markdown/durable-execution/guides-and-advanced/cancellation/events).                            |
| `Timeouts`    | `*ConfigTimeouts`    | Cancel a run that doesn't start or finish in time. See [timeouts](/docs-markdown/durable-execution/guides-and-advanced/cancellation/timeouts).         |
| `Checkpoint`  | `*checkpoint.Config` | Run steps with less latency between them. See [checkpointing](/docs-markdown/durable-execution/guides-and-advanced/checkpointing).                     |

Each `Config*` type is in the `inngestgo` package. See [pkg.go.dev](https://pkg.go.dev/github.com/inngest/inngestgo#FunctionOpts) for every field.