{"record":{"id":"2b495fcd85ec7a67","repo":"windmill-labs/windmill","slug":"cannot-parse-s-as-bool-invalid-literal","errorCode":null,"errorMessage":"Cannot parse '{s}' as bool: invalid literal","messagePattern":"Cannot parse '(.+?)' as bool: invalid literal","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/windmill-worker/src/pg_executor.rs","lineNumber":1667,"sourceCode":"        Value::String(s)\n            if arg_t == \"double\" || arg_t == \"double precision\" || arg_t == \"float8\" =>\n        {\n            s.parse::<f64>()\n                .map(|n| (Box::new(n) as Box<dyn ToSql + Sync + Send>, Type::FLOAT8))\n                .map_err(|e| anyhow::anyhow!(\"Cannot parse '{s}' as double: {e}\").into())\n        }\n        Value::String(s) if arg_t == \"oid\" => s\n            .parse::<u32>()\n            .map(|n| (Box::new(n) as Box<dyn ToSql + Sync + Send>, Type::OID))\n            .map_err(|e| anyhow::anyhow!(\"Cannot parse '{s}' as oid: {e}\").into()),\n        Value::String(s) if arg_t == \"bool\" || arg_t == \"boolean\" => {\n            // Accept the same literals Postgres' boolin() does.\n            let b = match s.to_ascii_lowercase().as_str() {\n                \"true\" | \"t\" | \"yes\" | \"y\" | \"1\" | \"on\" => true,\n                \"false\" | \"f\" | \"no\" | \"n\" | \"0\" | \"off\" => false,\n                _ => {\n                    return Err(\n                        anyhow::anyhow!(\"Cannot parse '{s}' as bool: invalid literal\").into(),\n                    )\n                }\n            };\n            Ok((Box::new(b), Type::BOOL))\n        }\n        Value::String(s) if arg_t == \"varchar\" || arg_t == \"character varying\" => {\n            Ok((Box::new(s.clone()), Type::VARCHAR))\n        }\n        // For arg_t in (json, jsonb): bind a JSON-encodable Value with the\n        // matching pg type. Falling through to TEXT here would assert TEXT\n        // and break query_typed_raw's encoder check.\n        // Object / Array (no `[]` suffix): bind as JSONB by default and\n        // JSON-stringify when the target is text-like.\n        //\n        // Note the asymmetry vs the Bool/Number arms above: we coerce to\n        // text on `matches!(typ, Typ::Str(_))` (which is true for both\n        // explicit `(text)` decls AND parser-default text), not on\n        // `explicit_text_target`. Reason: serialising a JSON object/array","sourceCodeStart":1649,"sourceCodeEnd":1685,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-worker/src/pg_executor.rs#L1649-L1685","documentation":"convert_val converts a JSON string argument into a tokio-postgres ToSql parameter. When the declared arg type is bool/boolean, it accepts exactly the literals Postgres' boolin() accepts (true/t/yes/y/1/on, false/f/no/n/0/off, case-insensitive) and raises 'Cannot parse ... as bool: invalid literal' for anything else. It exists to mirror PG's own boolean input rules client-side.","triggerScenarios":"A step argument typed bool receives a Value::String like 'True ' with trailing whitespace is fine only if lowercased? No — the match lowercases, so failures come from literals outside the accepted set: '0' works, but 'wahr', 'vrai', 'enabled', 'OK', or an empty string all fail.","commonSituations":"Local-language boolean words; flags from other systems such as 'Y'/'N' are fine but 'ENABLED'/'DISABLED' are not; form checkboxes sending 'checked'/'unchecked'; empty strings from unset toggles.","solutions":["Map application-level truthy/falsy values to one of PG's accepted literals ('true'/'false') before invoking the step","Pass an actual JSON boolean instead of a string so the boolean arm handles it directly","Add form validation so the toggle only emits 'true'/'false'","If empty means unset, convert to null and use a nullable boolean column instead of sending ''"],"exampleFix":"// before\nargs: { active: feature.enabled ? \"ENABLED\" : \"DISABLED\" }  // invalid literal\n// after\nargs: { active: feature.enabled ? \"true\" : \"false\" }","handlingStrategy":"type-guard","validationCode":"const PG_BOOL_LITERALS = new Set([\"true\",\"t\",\"yes\",\"y\",\"1\",\"on\",\"false\",\"f\",\"no\",\"n\",\"0\",\"off\"]);\nfunction assertPgBool(v) {\n  if (typeof v === \"boolean\") return;\n  if (typeof v !== \"string\" || !PG_BOOL_LITERALS.has(v.trim().toLowerCase())) {\n    throw new Error(`value ${JSON.stringify(v)} is not a valid PG boolean literal`);\n  }\n}","typeGuard":"function isPgBoolLiteral(v) {\n  return typeof v === \"boolean\" || (typeof v === \"string\" && PG_BOOL_LITERALS.has(v.trim().toLowerCase()));\n}","tryCatchPattern":"try {\n  await runPgStep(args);\n} catch (e) {\n  if (String(e.message).includes(\"as bool\")) {\n    throw new Error(`Boolean arg must be true/false (or t/yes/y/1/on, f/no/n/0/off), got: ${e.message}`);\n  }\n  throw e;\n}","preventionTips":["Send real JSON booleans instead of strings whenever possible","Map app-level flags (enabled/disabled, checked/unchecked) to true/false before invoking","Convert empty/unset toggles to null against a nullable column rather than \"\""],"tags":["postgresql","parse-error","boolean","type-conversion"],"backgroundTag":"invalid-boolean-literal","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}