windmill-labs/windmill · error
Cannot parse '{s}' as numeric: {e}
Error message
Cannot parse '{s}' as numeric: {e} What it means
convert_val converts a JSON string argument into a tokio-postgres ToSql parameter. When the declared arg type is numeric/decimal, it parses the string into a rust_decimal Decimal and wraps any parse failure as 'Cannot parse ... as numeric'. The comment in the source notes the value deliberately goes through client-side parsing so Postgres doesn't fail on a `<numeric> = text` comparison.
Source
Thrown at backend/windmill-worker/src/pg_executor.rs:1644
Error::ExecutionErr(format!("Cannot parse '{s}' as timestamptz: {e}"))
})?;
Ok((Box::new(datetime), Type::TIMESTAMPTZ))
}
Value::String(s) if arg_t == "bytea" => {
let bytes = engine::general_purpose::STANDARD
.decode(s)
.unwrap_or(vec![]);
Ok((Box::new(bytes), Type::BYTEA))
}
// Parse Strings into the matching native Rust type for the remaining
// recognised arg_ts that didn't have a dedicated arm. Without these,
// a string value lands in the generic Value::String fallback below
// (Box<String> + TEXT) and the server-side comparison
// `<numeric|real|...> = text` fails since PG has no implicit cast.
Value::String(s) if arg_t == "numeric" || arg_t == "decimal" => s
.parse::<Decimal>()
.map(|d| (Box::new(d) as Box<dyn ToSql + Sync + Send>, Type::NUMERIC))
.map_err(|e| anyhow::anyhow!("Cannot parse '{s}' as numeric: {e}").into()),
Value::String(s) if arg_t == "real" || arg_t == "float4" => s
.parse::<f32>()
.map(|n| (Box::new(n) as Box<dyn ToSql + Sync + Send>, Type::FLOAT4))
.map_err(|e| anyhow::anyhow!("Cannot parse '{s}' as real: {e}").into()),
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() {View on GitHub (pinned to e474e8803c)
Solutions
- Normalize the string to plain decimal notation (dot separator, no symbols or grouping) before invoking the step
- Strip currency symbols and thousand separators in the step script before passing the value
- Guard against NaN/Infinity upstream (they are not valid decimals) and substitute null or a sentinel
- Pass a JSON number when precision allows; reserve string only for high-precision values
Example fix
// before
args: { price: "€1.234,56" }
// after
args: { price: "1234.56" } // normalized decimal string Defensive patterns
Strategy: validation
Validate before calling
function assertDecimal(s) {
if (typeof s !== "string") return;
const normalized = s.trim().replace(/^[^\d-+]+/, "").replace(/,/g, m => m === "," ? "." : m);
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized) || /nan|infinity/i.test(s)) {
throw new Error(`value ${JSON.stringify(s)} is not a plain decimal`);
}
} Type guard
const isPlainDecimal = (v) => typeof v === "string" && /^[+-]?\d+(\.\d+)?$/.test(v.trim());
Try / catch
try {
await runPgStep(args);
} catch (e) {
if (String(e.message).includes("as numeric")) {
throw new Error(`Argument must be a plain decimal string, got: ${e.message}`);
}
throw e;
} Prevention
- Normalize locale formats (decimal comma, currency symbols) before passing
- Never stringify NaN/Infinity into a numeric arg
- Keep high-precision money values as strings but strip formatting first
When it happens
Trigger: A step argument typed numeric/decimal receives a Value::String that rust_decimal cannot parse: 'NaN', 'Infinity', '1,234.56' with thousand separator, an exponent form Decimal rejects, or non-numeric text.
Common situations: European decimal comma format ('12,50'); currency strings ('$19.99'); NaN/Infinity produced by JS math then stringified; numbers pasted from spreadsheets with grouping separators or currency symbols.
Related errors
- Cannot parse '{s}' as smallint: {e}
- Cannot parse '{s}' as integer: {e}
- Cannot parse '{s}' as bigint: {e}
- Cannot parse '{s}' as real: {e}
- Cannot parse '{s}' as double: {e}
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/31a56bbc1a0282ef.
Report an issue: GitHub.