Summary
A sweep across crates/, sdks/python, sdks/node, and sdks/java turns up a
long tail of parameters, struct fields, and config keys that carry a closed set
of values but are typed as bare str / String / &str. Most are guarded by a
runtime membership check (if x not in (...), VALID_ROLES.contains(...)), a few
by nothing at all — a typo is either a runtime ValueError the caller only sees in
production, or a silent no-op.
In several cases the enum already exists and the public API just doesn't use it
(DispatchOrder in core vs String order in the Java SDK; LogLevel in Python
vs String level in Java / level?: string in Node; NodeStatus in Python vs
raw string comparisons in the saga orchestrator).
This issue is an inventory + a proposal, not a single patch. It is naturally
splittable per domain.
Why bother
- Typos become compile/type errors instead of runtime exceptions or silent
no-ops. disabled_proxies=["requests"] today silently leaves the requests_session
handler registered.
- Discoverability: IDE autocomplete instead of reading a docstring for the
allowed values.
- Cross-SDK parity is currently invisible. Enumerating the sets immediately
exposed three divergences (see "Parity gaps surfaced" below) that nobody would
spot while every side is String.
- Removes hand-rolled validators and their duplicated error-message strings.
Inventory
A. Enum exists, public API still takes a string
| Value set |
Existing enum |
Raw-string surface |
Dispatch order (fifo / lifo) |
DispatchOrder — crates/taskito-core/src/storage/mod.rs:160 |
sdks/python/taskito/mixins/runtime_config.py:128 (order: str, checked at :145); sdks/java/.../Taskito.java:155 + DefaultTaskito.java:221 (String order) |
| Task-log level |
LogLevel — sdks/python/taskito/context.py:20 |
sdks/java/.../Taskito.java:334,336,344 (String level); sdks/node/src/queue.ts:1026 (level?: string); crates/taskito-core/src/storage/traits.rs:365 (level: &str) |
| Workflow node status |
NodeStatus — sdks/python/taskito/workflows/types.py:37 |
sdks/python/taskito/workflows/saga/orchestrator.py:261 and workflows/tracker/dag.py:71 compare raw strings instead of the enum |
| Workflow run state |
WorkflowState (py/java) |
sdks/java/.../Taskito.java:554 listWorkflowRuns(..., String state, ...); spi/QueueBackend.java:390 |
| Interception strategy |
Strategy — sdks/python/taskito/interception/strategy.py:6 |
sdks/python/taskito/mixins/runtime_config.py:34 register_type(strategy: str) — coerced via S(strategy) |
| Workflow condition |
WorkflowCondition — sdks/node/src/workflows/types.ts:29 |
sdks/java/.../workflows/Step.java:22 (String condition); sdks/python/taskito/workflows/builder.py:76,142 (condition: str | Callable | None) |
| Storage backend |
StorageBackend (storage/mod.rs:1118), Backend (storage/migrate.rs:33) |
sdks/python/taskito/app.py:136 (backend: str = "sqlite"); sdks/java/.../Taskito.java:604,694 (backend(String)) |
B. No enum anywhere — runtime-validated string
| Value set |
Values |
Where |
| Interception mode |
strict / lenient / off |
sdks/python/taskito/interception/interceptor.py:60, validated :65; sdks/python/taskito/app.py:141 |
Predicate on_false |
defer / cancel |
sdks/python/taskito/mixins/decorators.py:199, validated :305; stored as dict[str, str] at mixins/predicates.py:53 |
Pub/sub subscriber on_error |
retry / skip |
sdks/python/taskito/mixins/pubsub.py:163, validated :186; read back at mixins/_log_consumer.py:46,95 |
Gate on_timeout |
approve / reject |
sdks/python/taskito/workflows/builder.py:47, validated :301 — Java already has GateAction, Python doesn't |
| Dashboard user role |
admin / viewer |
sdks/python/taskito/dashboard/auth.py:59,173,233; sdks/node/src/dashboard/auth/store.ts:43,152,232; sdks/java/.../dashboard/auth/AuthStore.java:33,382 — three parallel VALID_ROLES sets |
| Topic subscription mode |
log / fanout |
consts at crates/taskito-core/src/storage/records.rs:159,161, validated crates/taskito-core/src/pubsub.rs:16; threaded as mode: &str through storage/traits.rs:298, crates/taskito-python/src/py_queue/pubsub.rs:62,271, crates/taskito-node/src/queue/pubsub.rs:194 |
| Webhook delivery status filter |
delivered / failed / dead / pending |
sdks/python/taskito/dashboard/handlers/webhook_deliveries.py:48 |
C. No enum, no validation — typo silently misbehaves
| Value set |
Values |
Where |
Failure mode |
| Built-in proxy handler ids |
file, logger, requests_session, httpx_client, boto3_client, gcs_client |
sdks/python/taskito/app.py:146 (disabled_proxies: list[str] | None) consumed at proxies/built_in.py:23-75 |
typo → handler stays registered, silently. The example that prompted this issue. |
| Workflow visualize format |
mermaid / dot |
sdks/python/taskito/workflows/run.py:118, workflows/builder.py:368 (fmt: str = "mermaid", branch at :378/:132) |
anything that isn't "dot" silently renders Mermaid |
| Worker status |
draining, … |
crates/taskito-core/src/storage/traits.rs:420 update_worker_status(_, status: &str); only writer is sdks/python/taskito/mixins/lifecycle.py:200 |
unvalidated free text persisted to storage; no canonical set is documented anywhere |
| Fan-out strategy |
each (fan-out) / all (fan-in) |
sdks/python/taskito/workflows/fan_out.py:10 (strategy: str) |
raises ValueError at execution time, i.e. after enqueue — Java already has FanMode |
Parity gaps surfaced by the sweep
Worth fixing alongside, since they only became visible by writing the sets down:
ResourceScope has three different shapes.
Python WORKER / TASK / THREAD / REQUEST (resources/definition.py:11) ·
Java WORKER / THREAD / TASK / REQUEST / POOLED (resources/ResourceScope.java) ·
Node "worker" | "task" | "pooled" (sdks/node/src/resources/types.ts:2).
No two agree.
DeliveryStatus: Node's union is delivered | failed | dead
(webhooks/types.ts:41), the Python dashboard filter also accepts pending
(dashboard/handlers/webhook_deliveries.py:48) — a value nothing appears to write.
- Java
LogLevel (logging/LogLevel.java) is the SDK's own logger level and
is package-private; the task-log level on the public Taskito surface is an
unconstrained String. Two unrelated concepts, one name.
Proposal
Per-language idiom, one wire form (the current lowercase string) so nothing on the
wire or in storage changes:
- Rust —
enum + serde(rename_all = "snake_case"), FromStr/as_str(). Keep
the &str storage-trait signatures for now if a churn-free step is preferred, but
parse at the boundary rather than string-comparing downstream.
- Python —
StrEnum (or (str, Enum), matching LogLevel/NodeStatus) and accept
Enum | str at public entry points for one release so existing string callers keep
working; drop the hand-rolled not in (...) checks.
- Node — string-literal union types (already the house style; see
events.ts,
resources/types.ts) plus an exported const array for runtime validation.
- Java —
enum with a wire() / fromWire() pair, matching the existing
FanMode / WorkflowState / GateAction pattern.
Non-goals. Open registries stay strings: user-registered proxy handler ids
(ProxyHandler.name, sdks/node/src/proxies/types.ts:31), resource names, task
names, queue names. Only the built-in proxy ids in disabled_proxies are a closed
set.
Suggested split
- Proxy handler ids +
disabled_proxies (the silent-no-op case) — smallest, highest signal.
DispatchOrder / task-log LogLevel / WorkflowState on the Java + Node public surfaces (enums already exist in core/Python).
- Python-side: interception mode,
on_false, on_error, gate on_timeout, visualize fmt, fan-out strategy.
- Dashboard
Role in all three SDKs — replaces three copies of VALID_ROLES.
- Pub/sub subscription mode + worker status in core.
- Reconcile the three
ResourceScope definitions (needs a decision on the canonical set first).
Summary
A sweep across
crates/,sdks/python,sdks/node, andsdks/javaturns up along tail of parameters, struct fields, and config keys that carry a closed set
of values but are typed as bare
str/String/&str. Most are guarded by aruntime membership check (
if x not in (...),VALID_ROLES.contains(...)), a fewby nothing at all — a typo is either a runtime
ValueErrorthe caller only sees inproduction, or a silent no-op.
In several cases the enum already exists and the public API just doesn't use it
(
DispatchOrderin core vsString orderin the Java SDK;LogLevelin Pythonvs
String levelin Java /level?: stringin Node;NodeStatusin Python vsraw string comparisons in the saga orchestrator).
This issue is an inventory + a proposal, not a single patch. It is naturally
splittable per domain.
Why bother
no-ops.
disabled_proxies=["requests"]today silently leaves therequests_sessionhandler registered.
allowed values.
exposed three divergences (see "Parity gaps surfaced" below) that nobody would
spot while every side is
String.Inventory
A. Enum exists, public API still takes a string
fifo/lifo)DispatchOrder—crates/taskito-core/src/storage/mod.rs:160sdks/python/taskito/mixins/runtime_config.py:128(order: str, checked at:145);sdks/java/.../Taskito.java:155+DefaultTaskito.java:221(String order)LogLevel—sdks/python/taskito/context.py:20sdks/java/.../Taskito.java:334,336,344(String level);sdks/node/src/queue.ts:1026(level?: string);crates/taskito-core/src/storage/traits.rs:365(level: &str)NodeStatus—sdks/python/taskito/workflows/types.py:37sdks/python/taskito/workflows/saga/orchestrator.py:261andworkflows/tracker/dag.py:71compare raw strings instead of the enumWorkflowState(py/java)sdks/java/.../Taskito.java:554listWorkflowRuns(..., String state, ...);spi/QueueBackend.java:390Strategy—sdks/python/taskito/interception/strategy.py:6sdks/python/taskito/mixins/runtime_config.py:34register_type(strategy: str)— coerced viaS(strategy)WorkflowCondition—sdks/node/src/workflows/types.ts:29sdks/java/.../workflows/Step.java:22(String condition);sdks/python/taskito/workflows/builder.py:76,142(condition: str | Callable | None)StorageBackend(storage/mod.rs:1118),Backend(storage/migrate.rs:33)sdks/python/taskito/app.py:136(backend: str = "sqlite");sdks/java/.../Taskito.java:604,694(backend(String))B. No enum anywhere — runtime-validated string
strict/lenient/offsdks/python/taskito/interception/interceptor.py:60, validated:65;sdks/python/taskito/app.py:141on_falsedefer/cancelsdks/python/taskito/mixins/decorators.py:199, validated:305; stored asdict[str, str]atmixins/predicates.py:53on_errorretry/skipsdks/python/taskito/mixins/pubsub.py:163, validated:186; read back atmixins/_log_consumer.py:46,95on_timeoutapprove/rejectsdks/python/taskito/workflows/builder.py:47, validated:301— Java already hasGateAction, Python doesn'tadmin/viewersdks/python/taskito/dashboard/auth.py:59,173,233;sdks/node/src/dashboard/auth/store.ts:43,152,232;sdks/java/.../dashboard/auth/AuthStore.java:33,382— three parallelVALID_ROLESsetslog/fanoutcrates/taskito-core/src/storage/records.rs:159,161, validatedcrates/taskito-core/src/pubsub.rs:16; threaded asmode: &strthroughstorage/traits.rs:298,crates/taskito-python/src/py_queue/pubsub.rs:62,271,crates/taskito-node/src/queue/pubsub.rs:194delivered/failed/dead/pendingsdks/python/taskito/dashboard/handlers/webhook_deliveries.py:48C. No enum, no validation — typo silently misbehaves
file,logger,requests_session,httpx_client,boto3_client,gcs_clientsdks/python/taskito/app.py:146(disabled_proxies: list[str] | None) consumed atproxies/built_in.py:23-75mermaid/dotsdks/python/taskito/workflows/run.py:118,workflows/builder.py:368(fmt: str = "mermaid", branch at:378/:132)"dot"silently renders Mermaiddraining, …crates/taskito-core/src/storage/traits.rs:420update_worker_status(_, status: &str); only writer issdks/python/taskito/mixins/lifecycle.py:200each(fan-out) /all(fan-in)sdks/python/taskito/workflows/fan_out.py:10(strategy: str)ValueErrorat execution time, i.e. after enqueue — Java already hasFanModeParity gaps surfaced by the sweep
Worth fixing alongside, since they only became visible by writing the sets down:
ResourceScopehas three different shapes.Python
WORKER / TASK / THREAD / REQUEST(resources/definition.py:11) ·Java
WORKER / THREAD / TASK / REQUEST / POOLED(resources/ResourceScope.java) ·Node
"worker" | "task" | "pooled"(sdks/node/src/resources/types.ts:2).No two agree.
DeliveryStatus: Node's union isdelivered | failed | dead(
webhooks/types.ts:41), the Python dashboard filter also acceptspending(
dashboard/handlers/webhook_deliveries.py:48) — a value nothing appears to write.LogLevel(logging/LogLevel.java) is the SDK's own logger level andis package-private; the task-log level on the public
Taskitosurface is anunconstrained
String. Two unrelated concepts, one name.Proposal
Per-language idiom, one wire form (the current lowercase string) so nothing on the
wire or in storage changes:
enum+serde(rename_all = "snake_case"),FromStr/as_str(). Keepthe
&strstorage-trait signatures for now if a churn-free step is preferred, butparse at the boundary rather than string-comparing downstream.
StrEnum(or(str, Enum), matchingLogLevel/NodeStatus) and acceptEnum | strat public entry points for one release so existing string callers keepworking; drop the hand-rolled
not in (...)checks.events.ts,resources/types.ts) plus an exportedconstarray for runtime validation.enumwith awire()/fromWire()pair, matching the existingFanMode/WorkflowState/GateActionpattern.Non-goals. Open registries stay strings: user-registered proxy handler ids
(
ProxyHandler.name,sdks/node/src/proxies/types.ts:31), resource names, tasknames, queue names. Only the built-in proxy ids in
disabled_proxiesare a closedset.
Suggested split
disabled_proxies(the silent-no-op case) — smallest, highest signal.DispatchOrder/ task-logLogLevel/WorkflowStateon the Java + Node public surfaces (enums already exist in core/Python).on_false,on_error, gateon_timeout, visualizefmt, fan-out strategy.Rolein all three SDKs — replaces three copies ofVALID_ROLES.ResourceScopedefinitions (needs a decision on the canonical set first).