Skip to main content
Aether’s hexagonal architecture defines ten extension interfaces beyond the executor, store, and broker. Each interface isolates a single infrastructure concern behind a minimal Go contract. All of them are wired into the engine via functional options, and all are optional except where noted. This page covers each interface, its method signatures, the circumstances under which you would implement it, and the corresponding WithXxx option.

idgen.Generator — unique ID generation

Required. Every WorkflowRun and TaskRun needs a globally unique string ID. The engine calls Generate whenever it creates one of these records.
The Context argument lets implementations embed structured information into the ID — for example, a UUID that encodes the workflow kind, or a Snowflake ID that embeds the task name as a prefix. Implementations that only need globally unique strings can ignore ctx entirely.When to implement: Always — WithIDGenerator is required. Your choices are UUIDs (github.com/google/uuid), Snowflake IDs, or a monotonic counter for testing.Engine option:
Playground example — AtomicIDGen:
AtomicIDGen generates monotonically increasing string IDs starting from "1". It is safe for concurrent use — atomic.Uint64.Add is a single instruction. In production, replace it with a UUID or a distributed ID scheme that is unique across engine replicas.
Optional. The engine uses an Evaluator for when conditions (task-level guards), repeatCondition (loop termination), phaseConditions (custom phase mapping), and valueFrom.expression (parameter computation). Without an evaluator, all expression fields must be absent or the engine returns an error.
env is a flat map[string]any containing all variables in scope at the call site: workflow inputs, task outputs, loop iteration variables, and any values contributed by registered vars.Source implementations. The return value is compared as a boolean for when/repeatCondition, or used as a string/numeric value for valueFrom.expression.When to implement: Any workflow that uses when, repeatCondition, phaseConditions, or valueFrom.expression requires an evaluator. Common choices: expr-lang/expr, google/cel-go, or a simple custom parser for constrained DSLs.Engine option:
Playground example — SimpleEvaluator:
SimpleEvaluator handles true, false, a == b, and a != b. Tokens wrapped in single or double quotes are treated as string literals; bare tokens are resolved from env.
Optional. The engine calls Notify at key moments in a workflow or task lifecycle. Implement Notifier to integrate with external systems — webhooks, message buses, observability platforms, or audit logs.
Hook types:Scope values: hook.ScopeWorkflow or hook.ScopeTask.Fan-out with CompositeNotifier: To send hooks to multiple destinations, compose notifiers:
CompositeNotifier.Notify calls all members and joins errors — individual failures do not prevent other notifiers from running.Engine option:
When to implement: When you need real-time notifications about workflow and task lifecycle events for monitoring, alerting, or downstream automation.
Optional. The Watcher detects when tasks or workflow runs exceed their configured deadlines and emits TimeoutEvent values for the engine to consume. The engine’s only job is to react to these events — it never polls the store for expired deadlines itself.
Design invariants from the source:
  • Events are delivered at-least-once. The engine’s timeout handler is idempotent.
  • The watcher does not modify any state — it only discovers and emits.
  • Deadline data lives in the store (store.TaskRun.Deadline, store.WorkflowRun.Deadline), so the watcher is stateless and survives engine restarts without losing pending timeouts.
Engine option:
When to implement: When any workflow or task uses the timeout field. Without a Watcher, timeout fields are stored but deadlines are never enforced.Playground example — PollingWatcher:
PollingWatcher scans the store at a fixed interval, comparing each active entity’s Deadline to time.Now(). It emits events to a buffered channel (capacity 256) and drops events if the buffer is full (consumer too slow).
Production deployments should replace PollingWatcher with a distributed timeout mechanism backed by Redis sorted sets, a database cron job with leader election, or a dedicated deadline service — something that survives engine process restarts and works across replicas.
Optional. Artifacts are binary blobs (files, archives, reports) produced or consumed by tasks. Implement Repository to integrate with object storage, local filesystem, or an HTTP endpoint.
config is opaque JSON parsed from the workflow’s artifact.source.config field. The model layer defines three config shapes: OSSSourceConfig, LocalSourceConfig, and HTTPSourceConfig — but your implementation may define its own schema.Engine option:
When to implement: When workflows use artifact inputs or outputs (artifacts fields on tasks or templates). The artifact interface is currently stored by the engine but not yet wired into the execution path — it is reserved for the upcoming artifact upload/download feature.
Optional. Implement Provider to resolve secretKeyRef parameter values at task dispatch time. This lets workflow authors reference secrets by name and key without embedding credential values in workflow documents.
name is the secret’s logical name (e.g. "my-db-creds"); key is the field within that secret (e.g. "password"). This maps naturally to Kubernetes Secrets, HashiCorp Vault, AWS Secrets Manager, or any key-value secret store.A workflow parameter using a secretKeyRef looks like:
Engine option:
When to implement: Whenever workflow parameters reference secretKeyRef. Without a provider, resolution of these parameters will fail.
Optional. Source contributes a flat key→value map to the workflow evaluation environment. Variables from registered sources are available as {{namespace.key}} placeholders in workflow template parameters.
Vars() is called each time the evaluation environment is built, so it may return dynamically computed values. Engine-level sources have lower priority than per-call sources — if WorkflowArgsSource and a custom source both produce the same key, the per-call value wins.Built-in: SystemSource:
Register it to make {{system.os}} and {{system.arch}} available in all workflow templates.Custom example — per-run tenant context:
Engine option:
Call WithVarsSource multiple times to register multiple sources. Sources are injected into every variable resolution call for every workflow run.
When to implement: When workflows need access to runtime environment data, deployment metadata, or any stable per-engine context that does not change between task executions.
Optional. In distributed deployments, workers register themselves with the registry at startup to announce their identity and supported executor types. The master uses the registry to find eligible workers for type-based task routing.
Workers call Register at startup with their Info (ID, supported executor types, and full schemas for each type). The engine can then call ListByExecutorType to route tasks to capable workers. Heartbeat keeps registrations alive; implementations may remove stale workers that miss heartbeats.
worker.Registry manages worker identity and capabilities. Task lifecycle (dispatch, fetch, complete) is still handled by broker.TaskBroker. These two interfaces are intentionally separate.
Engine option:
When to implement: In distributed deployments where the broker needs to route tasks to specific workers by executor type, or where you need to track which workers are alive and what they support.
Optional. ErrorSink is a non-blocking observation outlet for engine-internal errors. It never influences scheduling decisions — it exists purely to feed external monitoring and alerting systems.
Severity guide:Engine option:
When to implement: In production systems where you want structured visibility into engine-internal errors. Without a sink, errors on void callbacks and non-critical paths are silently discarded.Implementation requirements:
  • Must be safe for concurrent use.
  • Must return quickly. Offload long-running work (HTTP calls, disk I/O) to a background goroutine or a buffered channel to avoid blocking the engine’s scheduling loop.
Optional. Scheduler provides the scheduling backend for CronWorkflow resources. Without it, SubmitCronWorkflow, GetCronWorkflow, and related engine methods return ErrNotSupported.
method
Launches the scheduler’s background services. Returns immediately. Background work runs until Stop is called. Safe to call only once; idempotent implementations may log a warning on repeated calls.
method
Shuts down background services. Safe to call multiple times; only the first call takes effect.
method
Registers a cron entry. id is the CronWorkflow’s system-generated ID. schedule is a standard cron expression (five fields). timezone is an IANA timezone string (e.g. "UTC", "Asia/Shanghai"). callback is invoked on each schedule match — the engine provides this function and it submits a new WorkflowRun.
method
Unregisters a cron entry by ID. No-op if the ID is not found.
Engine lifecycle integration: Engine.Start calls Scheduler.Start; Engine.Stop calls Scheduler.Stop. During startup, the engine calls ListCronWorkflows on the store and re-registers each active cron entry with Add — this restores schedules after a crash or restart.Engine option:
When to implement: When you use CronWorkflow resources to trigger workflows on a schedule. A robust production implementation would use a distributed cron library with leader election to prevent duplicate triggers across engine replicas — for example robfig/cron for single-process, or a Redis-backed scheduler for distributed deployments.

Summary: required vs optional

These three options must be provided; aether.New will return an error without them.