Help us shape the future of Go! Take the 2026 Go Developer Survey

automations

package
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: AGPL-3.0 Imports: 32 Imported by: 0

Documentation

Overview

Package automations is the one place an Automation's steps are stored, validated and its lifecycle changed (ADR 0016). /site and /api handlers stay thin adapters over it; the step executor (internal/jobs) decodes the same Step format, so the storage JSON has a single definition. The steps live in versions (ADR 0049).

Index

Constants

View Source
const (
	StepEmail     = "email"
	StepWait      = "wait"
	StepApplyTag  = "apply_tag"
	StepRemoveTag = "remove_tag"
	StepHTTP      = "http"
)

Step kinds.

View Source
const (
	MaxHTTPURLBytes           = httpcall.MaxURLBytes
	MaxHTTPBodyBytes          = httpcall.MaxBodyBytes
	MaxHTTPHeaders            = httpcall.MaxHeaders
	MaxHTTPStepsPerAutomation = 10
)

Limits of an HTTP step (ADR 0039): they keep one step from exhausting the queue.

View Source
const ErrorKindInvalidBody = "invalid_body"

ErrorKindInvalidBody is the closed error kind of a body that does not render to valid JSON (a template error, invalid JSON or an oversized body).

Variables

View Source
var (
	// ErrNotFound: no such Automation in this Workspace.
	ErrNotFound = validation.Refuse("automations: automation not found", "errors.automation_not_found")
	// ErrInvalidStep: a step has an unknown type, a negative wait, or a tag step
	// without a tag name, or an email step whose content cannot render.
	ErrInvalidStep = validation.Refuse("automations: invalid step", "errors.automation_step_invalid")
	// ErrInvalidTrigger: the trigger is an infrastructure Event (a send or a delivery notice)
	// that no Automation may start from (ADR 0053).
	ErrInvalidTrigger = validation.Refuse("automations: invalid trigger", "errors.automation_trigger_invalid")

	// ErrNoVersion: the Automation has no version yet. Only an Automation made before versions
	// existed can be in that state, until the backfill job (internal/jobs) has run.
	ErrNoVersion = errors.New("automations: automation has no version yet")
)

Domain errors. Callers match with errors.Is.

View Source
var (
	// ErrContactNotFound: no such Contact in this Workspace.
	ErrContactNotFound = validation.Refuse("automations: contact not found", "errors.contact_not_found")
	// ErrInvalidBody: the body did not render to valid JSON within the size limit.
	ErrInvalidBody = httpcall.ErrInvalidBody
)

Functions

func Advance

func Advance(ctx context.Context, s *ent.Scoped, runID int64, from, to int, resumeAt *time.Time) error

Advance moves an active Enrollment standing on step from to step to, parking it until resumeAt (nil: no wait). It is a compare-and-swap on `active` and on the current step, so a step cannot overwrite an exit and a job delivered twice cannot move the Enrollment twice: lifecycle.ErrInvalidTransition when the Enrollment is no longer active or no longer on from.

func CancelPending

func CancelPending(ctx context.Context, s *ent.Scoped, contactID int64, purger events.QueuePurger) error

CancelPending cancels a Contact's pending Automation work (Erasure, ADR 0021): the queued step and HTTP-step jobs of the Contact's runs and the queued trigger evaluations that name the Contact. The runs themselves are Erasure's to delete (a failure's response excerpt sits on the run). It runs inside the caller's transaction (s and purger come from the same one); a job that is already running is not purged.

func Complete

func Complete(ctx context.Context, s *ent.Scoped, runID int64) error

Complete ends the Enrollment `completed`. lifecycle.ErrInvalidTransition when it is no longer active (an exit won), which the step executor treats as a finished step.

func Encode

func Encode(steps []Step) (string, error)

Encode serializes steps into the stored definition, dropping the fields that do not belong to each step's type, trimming tag names and sanitizing an email step's body.

func Enroll

func Enroll(ctx context.Context, s *ent.Scoped, automationID, contactID int64) (runID int64, enrolled bool, err error)

Enroll starts an Enrollment of the Contact in the Automation and returns its run id. enrolled is false when the Contact has been enrolled before (the common trigger-replay path) or another request won the race; nothing is created then.

func Exit

func Exit(ctx context.Context, s *ent.Scoped, automationID, contactID int64) (bool, error)

Exit ends the Contact's active Enrollment in the Automation (status exited, no pending resume). It reports whether an active Enrollment was ended; a finished or absent one is left alone, and so is one another writer moved first.

func ExitRun

func ExitRun(ctx context.Context, s *ent.Scoped, runID int64) error

ExitRun ends one Enrollment by run id, for the step executor that already holds the run (an ineligible destination exits it rather than walk on skipping mail). lifecycle.ErrInvalidTransition when it is no longer active.

func Fail

func Fail(ctx context.Context, s *ent.Scoped, runID int64, f Failure) error

Fail ends the Enrollment `failed`, keeping the reason. lifecycle.ErrInvalidTransition when it is no longer active.

func IsActive

func IsActive(ctx context.Context, s *ent.Scoped, runID int64) (bool, error)

IsActive reports, from a fresh read through the lifecycle table, whether the Enrollment can still move: a step that finds it exited, completed or failed stops quietly. A run that no longer exists (erased with its Contact) is not active either.

func IsInvalid

func IsInvalid(err error) bool

IsInvalid reports whether err refuses the Automation's input (a step or the trigger), which an API surface renders as 422.

func Loaded

func Loaded(ctx context.Context, s *ent.Scoped, preds ...predicate.Automation) ([]*ent.Automation, error)

Loaded returns the Workspace's Automations matching preds, each carrying its current steps (CurrentSteps). Archived Automations are not loaded: they enroll nobody and use nothing. A live-dependency check reads them to see which Tag, Integration or Sending domain an Automation's steps use (ADR 0049).

func RenderBody

func RenderBody(body string, c *ent.Contact) (string, error)

RenderBody renders an HTTP step's Liquid body for a Contact (nil: no bindings) and checks the result is valid JSON within the size limit. The Contact's strings are JSON-escaped in the bindings themselves, so a value holding quotes cannot change the structure.

func Stale

func Stale(err error) bool

Stale reports that err means the Enrollment is no longer there to move: another writer took it out of `active` (an exit won) or it was erased with its Contact. A step that gets it finishes quietly; nil and every other error are not stale.

func UsingTag

func UsingTag(ctx context.Context, s *ent.Scoped, name string) ([]*ent.Automation, error)

UsingTag returns the Workspace's Automations, archived ones included, whose current steps apply or remove the Tag by its name, in id order. An archived Automation is restorable, so it still names the Tag (ADR 0049).

Types

type CreateInput

type CreateInput struct {
	Name         string
	TriggerEvent string
	Steps        []Step
}

CreateInput is the data for a new Automation. It is always created as a draft: activating is a separate, deliberate operation.

type DryRunResult

type DryRunResult struct {
	Body      string
	ErrorKind string
	Detail    string
}

DryRunResult is a body rendered for one Contact, or the kind of error that stopped it. Nothing is called.

type Failure

type Failure struct {
	Kind    automationrun.FailureKind
	Status  int
	Excerpt string
}

Failure is what an Enrollment keeps of the step that failed it. The zero value records nothing beyond the `failed` status.

func FailureOfDelivery

func FailureOfDelivery(sendErr error) Failure

FailureOfDelivery is the record of a call to a third party that failed: the kind of the delivery error, with the answer's status and excerpt where the receiver gave one. An error that is not a delivery error is a network failure.

type Fencer

type Fencer func(ctx context.Context, s *ent.Scoped, runID int64, effect func(context.Context) error) error

Fencer runs a step's side effect (a mail, an HTTP call) so that it is ordered against an exit of the Enrollment: automations.Stale(err) when the Enrollment was no longer active when the effect would have started (then effect did not run), otherwise effect's own error.

func FenceOn

func FenceOn(client *ent.Client) Fencer

FenceOn is the Fencer of a database with real connections (ADR 0046): effect runs while a share lock is held on the Enrollment's row, taken only if the row is still `active`. Every move out of `active` is an UPDATE of that row, and an UPDATE waits for a share lock, so

  • an exit that arrives while the effect is in flight returns only after it finished: the effect is ordered before the exit, never after it;
  • an exit committed before the lock is seen under it (the lock's WHERE is re-checked once a concurrent update commits), so the effect never starts.

The lock lives on a transaction of its own that does nothing else, and effect runs on the pool's other connections as it always did: the step's compare-and-swap that follows (Advance, Complete, Fail) is made after the lock is released, so it cannot wait on itself, and the outbound claim, lease and idempotency key are untouched. The hold lasts as long as effect runs, which the job's context, the provider's and the HTTP client's timeouts bound; the transaction ends when effect returns or ctx is done. Only the lock is read-only, so there is nothing to commit.

The lock needs one pool connection beyond the effect's own for the effect's duration.

type Header = httpcall.Header

Header is one request header of an HTTP step. In a stored step Value is ciphertext. In a saved step, an empty Value keeps the stored value of the header with the same name.

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is the automations module.

func New

func New(cipher *secrets.Cipher, bus *events.Bus) *Module

New builds the automations module. It holds no client: every call takes the Workspace's scoped client (ADR 0017). The cipher seals an HTTP step's secrets; the bus opens the transaction that saves an Automation together with its steps.

func (*Module) Activate

func (m *Module) Activate(ctx context.Context, s *ent.Scoped, id int64) (*ent.Automation, error)

Activate starts enrolling Contacts into the Automation.

func (*Module) Archive

func (m *Module) Archive(ctx context.Context, s *ent.Scoped, id int64) error

Archive discards an Automation (ADR 0049): it leaves the lists and enrolls nobody, and its Enrollments stay. An Automation with active Enrollments is refused with an *inuse.Error carrying their count. Archiving an archived Automation changes nothing.

func (*Module) Create

func (m *Module) Create(ctx context.Context, s *ent.Scoped, in CreateInput) (*ent.Automation, error)

Create stores a new draft Automation with its first version.

func (*Module) Deactivate

func (m *Module) Deactivate(ctx context.Context, s *ent.Scoped, id int64) (*ent.Automation, error)

Deactivate stops new enrollments; in-flight Enrollments finish.

func (*Module) Delete

func (m *Module) Delete(ctx context.Context, s *ent.Scoped, id int64) error

Delete purges an Automation that has no Enrollments and has sent nothing, with its versions (ADR 0049). One with any Enrollment or Outbound message is history and is archived instead: an *inuse.Error carrying their counts. The messages count apart from the Enrollments, because Contact erasure removes a Contact's Enrollments and keeps the mail they sent.

func (*Module) DryRun

func (m *Module) DryRun(ctx context.Context, s *ent.Scoped, contactID int64, body string) (DryRunResult, error)

DryRun renders a body for a Contact of the Workspace and validates it without calling anything.

func (*Module) Enrollment

func (m *Module) Enrollment(ctx context.Context, s *ent.Scoped, automationID, id int64) (*ent.AutomationRun, error)

Enrollment returns one Enrollment of an Automation. ErrNotFound when the Workspace has no such Automation, or the Automation no such Enrollment.

func (*Module) Enrollments

func (m *Module) Enrollments(ctx context.Context, s *ent.Scoped, automationID int64, status string, p pagination.Params) (pagination.Page[*ent.AutomationRun], error)

Enrollments returns one page of an Automation's Enrollments, newest first, optionally only those in one status (an empty status means all). ErrNotFound when the Workspace has no such Automation.

func (*Module) Get

func (m *Module) Get(ctx context.Context, s *ent.Scoped, id int64) (*ent.Automation, error)

Get returns one Automation of the Workspace, archived or not: an archived Automation is still addressable (viewed, restored, its Enrollments listed).

func (*Module) List

func (m *Module) List(ctx context.Context, s *ent.Scoped, p pagination.Params, archived bool) (pagination.Page[*ent.Automation], error)

List returns one page of the Workspace's Automations, newest first: the kept ones, or with archived the archived ones.

func (*Module) OpenHTTP

func (m *Module) OpenHTTP(s Step) (OpenedHTTP, error)

OpenHTTP decrypts a stored HTTP step's URL, header values and secret.

func (*Module) ReportOf

func (m *Module) ReportOf(ctx context.Context, s *ent.Scoped, id int64) (Report, error)

ReportOf counts, leaving out machine engagement (ADR 0053), the `email.opened` and `email.clicked` Events that carry the Automation, and the clicks by link.

func (*Module) Unarchive

func (m *Module) Unarchive(ctx context.Context, s *ent.Scoped, id int64) error

Unarchive brings an archived Automation back to the lists. Restoring one that is not archived changes nothing.

func (*Module) Update

func (m *Module) Update(ctx context.Context, s *ent.Scoped, id int64, in UpdateInput) (*ent.Automation, error)

Update changes the Automation's name, trigger or steps. The status is untouched.

type OpenedHTTP

type OpenedHTTP = httpcall.Opened

OpenedHTTP is an HTTP step's secrets in the clear, for the executor.

type Report

type Report struct {
	Opens, Clicks int
	// Delivered, Bounced and Complained count the Automation's Outbound messages by their
	// delivery result stamp (ADR 0053), so they do not shrink when Events expire.
	Delivered, Bounced, Complained int
	// Captured counts the messages that went through a sender delivering nowhere outside (a Test
	// inbox): captured, not delivered.
	Captured int
	// Links counts the clicks by link, most clicked first.
	Links []linkclicks.Clicks
}

Report is an Automation's engagement figures. Every open and click is an Event (ADR 0053), so these are repeat counts, and they cover the Event retention window.

type Step

type Step struct {
	Type    string `json:"type"`
	Subject string `json:"subject,omitempty"`
	Body    string `json:"body,omitempty"`
	// Doc is the editor's JSON document an email step's Body was built from, and EditorVersion
	// the editor that rendered it (ADR 0040). A step written as HTML has neither. A step being
	// saved carries a Doc or a Body, not both: the save renders the Doc to the Body.
	Doc           json.RawMessage `json:"doc,omitempty"`
	EditorVersion string          `json:"editorVersion,omitempty"`
	Seconds       int             `json:"seconds,omitempty"`
	Tag           string          `json:"tag,omitempty"`

	// HTTP steps (ADR 0039) hold ID, URL, Headers, Body (a Liquid JSON template) and
	// Secret. Stored, URL, header values and Secret are ciphertext, sealed per field;
	// in a step being saved, an empty one keeps the stored value of the step with the
	// same ID, and ClearSecret drops the signing secret.
	ID          string   `json:"id,omitempty"`
	URL         string   `json:"url,omitempty"`
	Headers     []Header `json:"headers,omitempty"`
	Secret      string   `json:"secret,omitempty"`
	ClearSecret bool     `json:"-"`
}

Step is one node of an Automation's linear sequence, in the executor's on-disk JSON form. Email steps carry Subject and Body (HTML), wait steps Seconds, tag steps Tag (the Tag name).

func CurrentSteps

func CurrentSteps(a *ent.Automation) ([]Step, error)

CurrentSteps decodes the steps of the Automation's latest version, which the Automations this module returns carry. An Automation that was loaded some other way has none.

func Decode

func Decode(definition string) ([]Step, error)

Decode parses a stored definition. An empty definition has no steps.

type UpdateInput

type UpdateInput struct {
	Name         *string
	TriggerEvent *string
	Steps        *[]Step
}

UpdateInput changes an Automation; nil fields are left alone.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL