When one application uses an engine for workflows and scheduled tasks, a host owns their shared connection and execution endpoint. Pass one Temporal or Restate host to both drivers, then wire its lifecycle once. Memory drivers have no shared host; Hatchet is pending and unpublished.
This page owns lifecycle for both workflow drivers and scheduler drivers. Driver-specific registration belongs on those pages. Examples assume an existing application composer and a validated workflow registry.
Registration Order
Construct the host, register or bind all work, then start the host. Construction
does no network I/O. A workflow NewRunner and a scheduler constructor add work
to the host; a workflow NewDriver only controls runs. Late registration returns
the vendor’s ErrStarted.
Inside a composer, register cleanup at the acquiring owner, before later
construction can fail. Use composer.Must for fallible construction there;
startup callbacks complete before application runners begin. See
Lifecycle and Ownership.
Builder callbacks run synchronously once, in order, on one fresh draft. Nil entries panic; expanded nil or empty slices supply no edits. Publication detaches owned settings and containers from the retained draft, not native SDK internals, hooks, clients, or captured services. Finish configuring shared native objects before publishing them.
Temporal
The arkoren.dev/driver/temporal host shares a client, task queue, and worker.
Within your composer, create and register it before either driver:
import "arkoren.dev/driver/temporal"
host := temporal.New(func(host *temporal.Builder) {
host.HostPort("temporal:7233")
host.TaskQueue("app")
})
composer.Cleanup(host.Stop)
Register workflow and schedule work on that same host. Here registry already
contains your workflows and steps:
import (
"arkoren.dev/driver/temporal/temporalschedule"
"arkoren.dev/driver/temporal/temporalworkflow"
)
runner := composer.Must(temporalworkflow.NewRunner(host, registry))
schedules := composer.Must(temporalschedule.New(host))
The runner’s Driver supplies the workflow client; schedules supplies the
scheduler driver. Neither starts a worker. Add host lifecycle and readiness hooks:
composer.Startup(host.Start)
composer.Check("temporal", host.Ping)
Start dials, applies registrations, and starts the worker. Ping checks native API reachability, not worker liveness or scheduler dispatch readiness. A wrong address fails startup rather than waiting for the first client request. The scheduler’s own run lifecycle is separate and uses the already-started host.
Connection Policy
Defaults are HostPort("localhost:7233"), namespace default, and task queue
arkoren. Configure TLS, credentials, interceptors, and other native settings
through ClientOptions and WorkerOptions. When dialing, host address and
namespace override matching native fields, including when using host defaults.
The host adapts slog.Default() when no logger is supplied; an explicit native
client logger takes precedence. SDK option records are value copies, not
recursive snapshots.
If you already own a native client, wrap it instead:
host := temporal.NewFrom(client, func(host *temporal.Builder) {
host.TaskQueue("app")
})
Stop leaves this borrowed client open. Only TaskQueue, WorkerOptions,
WorkerStopTimeout, and NoWorker apply; host logger and connection settings do
not. Configure the borrowed client’s logger before wrapping it: the worker
inherits that logger. host.Client() is available immediately for NewFrom,
but is nil before Start for a dialing host. A nil borrowed client fails startup.
Shutdown Boundaries
Serialize Start and Stop; call Stop after Start returns. An already-canceled startup context acquires nothing. Native worker startup has no context parameter, so cancellation during that call is observed only after it returns, followed by resource cleanup. Registration callbacks and SDK hooks must return. Registration panics propagate after owned-client cleanup.
WorkerStopTimeout defaults to ten seconds; zero restores the default and a
negative host value disables grace. A nonzero native
WorkerOptions.WorkerStopTimeout wins. Native grace can apply separately to
internal workers before activity contexts are canceled: it is not a total
shutdown deadline and cannot terminate arbitrary callbacks.
Stop starts worker cleanup even with an expired context, then waits only until its context ends. Cleanup continues afterward. The owned client closes only after worker cleanup returns; a context error does not prove cleanup finished. Repeated Stop calls wait on the same cleanup using their own contexts. Borrowed clients remain yours to close.
See the temporal host API.
Restate
Restate reaches an endpoint in your application rather than polling a worker. The host therefore has three lifecycle operations: Start opens a listener, Serve handles requests, and Stop releases it.
Configure ingress/admin URLs that your process can reach, and an advertised URL
the Restate server can reach. Before constructing the host, obtain the server’s
request-identity public keys (publickeyv1_...):
import "arkoren.dev/driver/restate"
host := restate.New(func(host *restate.Builder) {
host.Addr(":9080")
host.IngressURL("http://restate:8080")
host.AdminURL("http://restate:9070")
host.AdvertisedURL("http://app:9080")
host.IdentityKeys(identityKeys...)
host.Register(true)
})
composer.Cleanup(host.Stop)
Here identityKeys is your configured []string of server public keys. A serving
host without keys fails Start with restate.ErrNoIdentityKeys.
AllowInsecure(true) skips verification and permits anyone who can reach the
listener to invoke handlers; use it only for
isolated local development, never as a production
startup repair. The endpoint supports HTTP/1.1 and unencrypted HTTP/2; establish
production network access controls and transport protection separately.
Bind workflow and scheduler services to this host before startup, using their constructors from the respective Drivers pages. Then add all three hooks:
composer.Startup(host.Start)
composer.Run(host.Serve)
composer.Check("restate", host.Ping)
Start builds the endpoint and opens the listener without serving. Serve begins request handling, optionally registers the deployment, and blocks until the application stops. Stop also owns an opened listener if Serve never runs, such as during startup rollback. Canceling Serve or a registration failure does not replace cleanup.
Ping checks the admin API’s health, not ingress reachability, registration,
successful workflow delivery, or persisted activation of the
preflight-retention profile. Verify an actual
run when delivery readiness matters. The default advertised URL is
http://localhost plus the listen address; that only works when the server can
reach the application there.
Deployment Policy
Register(true) asks Serve to register a deployment; otherwise operators must
register it. Each rollout creates a new deployment. RegisterForce overwrites
a deployment at the same URL and is appropriate only when none of the old work
is in flight. RegisterBreaking permits registration that removes services.
Neither option makes incompatible journals replay-safe. Use AdminAuthKey for
a protected admin API and AuthKey for protected ingress.
Drain workflow runs that used the earlier Child implementation on their original deployment before upgrading to its rejection; there is no automatic history migration. See workflow rollout limits.
When the same host serves schedules, preserve retained generations and finish in-flight Remove invocations on the old deployment before replacing an incompatible journal. If an older deployment removed an entry, drain its pending ticks before reusing its name. The Restate scheduler guidance owns those schedule-specific constraints.
Shutdown Boundaries
Stop waits for active HTTP requests until its context expires. It then closes remaining connections and returns the shutdown error. Closing connections cancels request contexts, not arbitrary Go code that ignores cancellation. The shared HTTP client remains caller-owned and is not closed by endpoint cleanup.
host.Query runs admin SQL; restrict its credentials and never accept arbitrary
queries from untrusted callers. See the
restate host API.
Client-Only Hosts
A web process can start work without serving it. For Temporal, set
temporal.Builder.NoWorker(true) and construct temporalworkflow.NewDriver.
For Restate, set restate.Builder.ClientOnly(true) and construct
restateworkflow.NewDriver. These host settings skip the worker or listener;
native connection and authentication still belong to the host.
Use the workflow driver’s client-only setup, not NewRunner. Temporal rejects a runner on a NoWorker host. Scheduler drivers register work, so construct those in the serving process instead. Match codec, prefix, and native routing across producers and workers.
A host with no registered work also behaves client-only. NoWorker defaults false; setting it false later restores permission but never forces a worker without registrations. Temporal’s WorkerDisabled reports the stored flag, not liveness.
Continue with Running Workflows once the host is started, or return to Scheduled Tasks for scheduler use.