Lint rules
The workflow sandbox has authoring rules that a linter can see; this package ships them as an ESLint-compatible plugin, loadable by oxlint's jsPlugins.
The authoring rules
The whole Effect program runs inside the Temporal workflow sandbox. Activity.make is a typed seam, not a Temporal Activity — durability comes from the Temporal activity proxies, timers, and signals the effects call, each memoized in history. Hence:
- All I/O goes through a Temporal activity — a declared activity call (
yield* Charge(payload)) orcallRawActivity. Anything else is nondeterministic on replay. A rawEffect.promise(() => acts.foo())works but is not cancelled on interrupt. Effect.promisecallbacks must be zero-arity — non-zero arity makes Effect allocate anAbortControllerper call, which the sandbox does not provide.- No module-level mutable state in workflow code — under the worker's default
reuseV8Context, module-level variables are shared across every workflow instance on a thread. Keep run state inside the handler. - Never mix the halves — a module must not import both the sandbox half (
@temporalio/workflow,engine-sandbox) and the client half (@temporalio/client,engine-client): they can never share a process. - Evaluate versions on the main fiber — version / versioned markers evaluated inside forks or races make marker order nondeterministic.
- Author with the definition module — the pre-0.4.0 surface (
typed-activity,versioning, the primitivemakeconstructors, the per-primitiveengine-sandboxcalls) was removed in 0.5.0; a stale import fails to resolve, and this rule tells you what replaced it.
Setup
Extend a shipped preset:
// .oxlintrc.json
{
"extends": ["./node_modules/@springbird/effect-temporal/oxlint-presets/recommended.json"]
}Or configure the rules directly:
{
"jsPlugins": ["@springbird/effect-temporal/lint"],
"rules": {
"effect-temporal/zero-arity-effect-promise": "error",
"effect-temporal/no-module-level-mutable": "error",
"effect-temporal/no-mixed-halves": "error",
"effect-temporal/prefer-call-temporal-activity": "warn",
"effect-temporal/versioning-on-main-fiber": "error",
"effect-temporal/prefer-definition": "error"
}
}Two presets ship: recommended (all six rules, prefer-call-temporal-activity as a warning) and correctness (only the hard-error sandbox rules — no migration rule).
Scope
A file counts as workflow code when it imports @temporalio/workflow, the bundle module, or the engine-sandbox module — the rules are inert elsewhere, so enabling them repo-wide is safe. no-mixed-halves applies everywhere by nature. versioning-on-main-fiber has one more trigger: importing version or versioned from the definition module marks the file for that rule (alias-aware), since definition-authored handler modules deliberately import nothing engine-shaped. The other sandbox rules cannot see such modules — a handler that needs them linted can live next to its bundle entry, which imports bundle.
prefer-definition applies everywhere: it keys off the import source alone (the package specifier or a relative path to one of this package's modules), and its message names the replacement — defineActivity for TypedActivity.make, versioned for Versioning.match, the declaration's .take for takeMailbox, codecsFor from wire, bundle for a workflowBundle import from engine-sandbox, and so on. Because it is an error in recommended, oxlint exits non-zero on any file still importing a removed symbol — a migration guide that runs as a lint.
The remaining footguns — drain mailboxes before continueAsNew, respond to updates before completion — are runtime-shaped and covered by runtime guards and the guide instead.
| Rule | Catches |
|---|---|
zero-arity-effect-promise | Effect.promise((signal) => ...) in workflow code |
no-module-level-mutable | module-level let/var in workflow code |
no-mixed-halves | one module importing both process halves |
prefer-call-temporal-activity | raw Effect.promise where a cancellable call belongs |
versioning-on-main-fiber | version / versioned / Versioning.* inside fork / race / all |
prefer-definition | any import of the pre-0.4.0 surface (removed in 0.5.0), with its replacement |