windmill-labs/windmill · error

Invalid WebSocket runnable path: {}

Error message

Invalid WebSocket runnable path: {}

What it means

The WebSocket trigger listener resolves the runnable (script/flow) referenced by a trigger's path and rewrites it into a WebSocket URL for its consumer. When the configured runnable path does not match the expected workspace path format (u/, f/, g/ or hub/ prefixed), the listener refuses to connect and throws this error instead of building an invalid URL.

Source

Thrown at backend/windmill-trigger-websocket/src/listener.rs:188

                    _ = self.loop_ping(&db, listening_trigger, err_message.clone(), Some(
                    "Waiting on runnable to return WebSocket URL...".to_string()
                )) => {
                        return Ok(None);
                    },
                    url_result = {
                        let authed = listening_trigger.authed(db, "ws").await?;
                        let args = listening_trigger.trigger_config.url_runnable_args.as_ref().map(|r| &r.0);
                        let path = url.splitn(2, ':').nth(1).unwrap();
                        get_url_from_runnable_value(path, url.starts_with("$flow:"), db, authed, args, &listening_trigger.workspace_id)
                    } => match url_result {
                        Ok(url) => Cow::Owned(url),
                        Err(err) => {
                            return Err(anyhow::anyhow!("Error getting WebSocket URL from runnable after 5 tries: {:?}", err).into());
                        }
                    },
                }
            } else {
                return Err(anyhow::anyhow!("Invalid WebSocket runnable path: {}", url).into());
            }
        } else {
            Cow::Borrowed(&url)
        };

        let validated = validate_websocket_url_for_ssrf(&connect_url).await?;

        // Gateway endpoints are often fronted by an edge proxy (e.g. Cloudflare)
        // that sporadically answers the upgrade request with a transient 5xx
        // instead of `101 Switching Protocols`, and a `get_consumer` error
        // disables the trigger until a human re-enables it — so retry transient
        // failures with backoff before giving up. The caller runs `loop_ping`
        // concurrently so `last_server_ping` stays alive across the sleeps, and
        // killpill cancels this future between awaits.
        let mut attempt = 0;
        loop {
            attempt += 1;
            match connect_async_with_proxy(&*connect_url, validated.pinned_addrs()).await {

View on GitHub (pinned to e474e8803c)

Solutions

  1. Fix the trigger's runnable path in the trigger settings to a valid workspace path (u/, f/, g/ or hub/ prefixed)
  2. Check the trigger definition via the API (GET /api/w/{workspace}/triggers/websocket) and correct the path field
  3. Re-create the trigger pointing at an existing script or flow, then restart the worker/listener

Example fix

// before
runnable_path: "/scripts/listen.py"
// after
runnable_path: "u/admin/listen.py"
Defensive patterns

Strategy: validation

Validate before calling

fn is_valid_ws_runnable_path(url: &str) -> bool {
    url.starts_with("u/") || url.starts_with("f/") || url.starts_with("g/") || url.starts_with("hub/")
}
// call before creating/updating the websocket trigger

Prevention

When it happens

Trigger: Creating or editing a websocket trigger whose resource_path / runnable path points to something like '/local/script', 'myscript', or an absolute filesystem path instead of 'u/user/my_script' or 'f/flows/x'. The check happens in get_consumer during trigger startup.

Common situations: Hand-editing trigger configuration, importing triggers from another instance where path conventions differ, typos in the path, or programmatically deploying triggers with paths that were never validated against workspace conventions.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/a60011ebeb606898. Report an issue: GitHub.