shareAI-lab/learn-claude-code · error · WorkflowInputError

invalid workflow runId

Error message

invalid workflow runId

What it means

validate_run_id enforces the canonical shape ^wf_[A-Za-z0-9][A-Za-z0-9._-]{0,63}_[0-9a-f]{16}$ — the 'wf_' prefix, a slug body of at most 64 characters, and a 16-lowercase-hex suffix. Any run_id that is not a string, or deviates from that shape, raises WorkflowInputError. Internally it also acts as a backstop on create_run_id output: because the slug portion shares the 64-character budget with the fixed affixes, a workflow name longer than 46 characters makes even a freshly generated id invalid.

Source

Thrown at s16_workflow_runtime/code.py:75

    for _ in range(32):
        run_id = validate_run_id(create_run_id(meta))
        snapshot_path = STORE / f"{run_id}.json"
        try:
            fd = os.open(snapshot_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
        except FileExistsError:
            continue
        os.close(fd)
        return run_id
    raise WorkflowInputError("could not allocate a unique workflow runId")


def create_task_id(run_id) -> str:
    return f"local_workflow_{run_id}"


def validate_run_id(run_id):
    if not isinstance(run_id, str) or not RUN_ID_RE.fullmatch(run_id):
        raise WorkflowInputError("invalid workflow runId")
    return run_id


# -- Errors --
class WorkflowInputError(Exception):
    """Bad workflow, metadata, or schema input."""


_run_locks_guard = threading.Lock()
_run_locks: dict[str, threading.Lock] = {}


@contextmanager
def workflow_run_lock(run_id: str):
    """Hold one run across threads and host processes for its full lifecycle."""
    with _run_locks_guard:
        local_lock = _run_locks.setdefault(run_id, threading.Lock())
    if not local_lock.acquire(blocking=False):

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Only use ids returned by reserve_run_id/run; never hand-build them
  2. Keep workflow meta.name to 46 characters or fewer so generated ids always validate
  3. If resuming, copy the id verbatim (lowercase hex, full 16 chars) from the store listing

Example fix

# before
run_id = f"wf_{workflow_name}_{uuid4().hex[:12]}"  # wrong shape, wrong hex length

# after
run_id = reserve_run_id(meta)  # wf_<slug>_<16 lowercase hex>, pre-validated
Defensive patterns

Strategy: validation

Validate before calling

import re
RUN_ID_RE = re.compile(r"^wf_[A-Za-z0-9][A-Za-z0-9._-]{0,63}_[0-9a-f]{16}$")

def is_valid_run_id(run_id) -> bool:
    return isinstance(run_id, str) and bool(RUN_ID_RE.fullmatch(run_id))

assert is_valid_run_id(run_id), f"malformed run id: {run_id!r}"

Type guard

def is_valid_run_id(run_id) -> bool:
    import re
    return isinstance(run_id, str) and bool(re.fullmatch(r"wf_[A-Za-z0-9][A-Za-z0-9._-]{0,63}_[0-9a-f]{16}", run_id))

Try / catch

try:
    validate_run_id(run_id)
except WorkflowInputError:
    # do not hand-repair ids; list the store and pick a real one, or reserve a new one
    run_id = reserve_run_id(meta)

Prevention

When it happens

Trigger: Passing a user-supplied or hand-built run_id like 'my-run' or 'WF_docs_abc123' to resume(). Copying a run id with the hex suffix uppercased or truncated. Naming a workflow with 47+ characters, since wf_ + name + _ + 16 hex exceeds the pattern.

Common situations: Scripts that store run ids in configs and resume them later, with edits/corruption in transit. Teams assuming the 64-char WORKFLOW_NAME_RE budget applies fully to run ids. Manually constructing ids instead of using reserve_run_id.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/9074e6111bac33e6. Report an issue: GitHub.