windmill-labs/windmill · error

Cannot parse '{s}' as bool: invalid literal

Error message

Cannot parse '{s}' as bool: invalid literal

What it means

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.

Source

Thrown at backend/windmill-worker/src/pg_executor.rs:1667

        Value::String(s)
            if arg_t == "double" || arg_t == "double precision" || arg_t == "float8" =>
        {
            s.parse::<f64>()
                .map(|n| (Box::new(n) as Box<dyn ToSql + Sync + Send>, Type::FLOAT8))
                .map_err(|e| anyhow::anyhow!("Cannot parse '{s}' as double: {e}").into())
        }
        Value::String(s) if arg_t == "oid" => s
            .parse::<u32>()
            .map(|n| (Box::new(n) as Box<dyn ToSql + Sync + Send>, Type::OID))
            .map_err(|e| anyhow::anyhow!("Cannot parse '{s}' as oid: {e}").into()),
        Value::String(s) if arg_t == "bool" || arg_t == "boolean" => {
            // Accept the same literals Postgres' boolin() does.
            let b = match s.to_ascii_lowercase().as_str() {
                "true" | "t" | "yes" | "y" | "1" | "on" => true,
                "false" | "f" | "no" | "n" | "0" | "off" => false,
                _ => {
                    return Err(
                        anyhow::anyhow!("Cannot parse '{s}' as bool: invalid literal").into(),
                    )
                }
            };
            Ok((Box::new(b), Type::BOOL))
        }
        Value::String(s) if arg_t == "varchar" || arg_t == "character varying" => {
            Ok((Box::new(s.clone()), Type::VARCHAR))
        }
        // For arg_t in (json, jsonb): bind a JSON-encodable Value with the
        // matching pg type. Falling through to TEXT here would assert TEXT
        // and break query_typed_raw's encoder check.
        // Object / Array (no `[]` suffix): bind as JSONB by default and
        // JSON-stringify when the target is text-like.
        //
        // Note the asymmetry vs the Bool/Number arms above: we coerce to
        // text on `matches!(typ, Typ::Str(_))` (which is true for both
        // explicit `(text)` decls AND parser-default text), not on
        // `explicit_text_target`. Reason: serialising a JSON object/array

View on GitHub (pinned to e474e8803c)

Solutions

  1. Map application-level truthy/falsy values to one of PG's accepted literals ('true'/'false') before invoking the step
  2. Pass an actual JSON boolean instead of a string so the boolean arm handles it directly
  3. Add form validation so the toggle only emits 'true'/'false'
  4. If empty means unset, convert to null and use a nullable boolean column instead of sending ''

Example fix

// before
args: { active: feature.enabled ? "ENABLED" : "DISABLED" }  // invalid literal
// after
args: { active: feature.enabled ? "true" : "false" }
Defensive patterns

Strategy: type-guard

Validate before calling

const PG_BOOL_LITERALS = new Set(["true","t","yes","y","1","on","false","f","no","n","0","off"]);
function assertPgBool(v) {
  if (typeof v === "boolean") return;
  if (typeof v !== "string" || !PG_BOOL_LITERALS.has(v.trim().toLowerCase())) {
    throw new Error(`value ${JSON.stringify(v)} is not a valid PG boolean literal`);
  }
}

Type guard

function isPgBoolLiteral(v) {
  return typeof v === "boolean" || (typeof v === "string" && PG_BOOL_LITERALS.has(v.trim().toLowerCase()));
}

Try / catch

try {
  await runPgStep(args);
} catch (e) {
  if (String(e.message).includes("as bool")) {
    throw new Error(`Boolean arg must be true/false (or t/yes/y/1/on, f/no/n/0/off), got: ${e.message}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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