windmill-labs/windmill · error
{webhook_key} must be a URL string, got {kind}
Error message
{webhook_key} must be a URL string, got {kind} What it means
The same declarative settings sync, but for a github_app_webhook_base_url that is present yet not a JSON string at all (bool, number, array, object, or other non-string value). The message deliberately names only the JSON kind and never prints the value, because the submitted content could carry a secret into sync-config output and operator logs.
Source
Thrown at backend/windmill-common/src/instance_config.rs:1327
// bool/number/object through as if the key were absent, and the diff below would
// then persist it — where the HTTP path answers "must be a URL string".
match desired.get(webhook_key) {
None | Some(serde_json::Value::Null) => {}
Some(serde_json::Value::String(s)) if s.trim().is_empty() => {}
Some(serde_json::Value::String(s)) => crate::global_settings::validate_webhook_base_url(s)
.map_err(|e| anyhow::anyhow!("{webhook_key}: {e}"))?,
// Names the JSON kind rather than printing it: this is the last message on
// this path that could report submitted content, and an object or array could
// carry a secret into `sync-config` output and operator logs.
Some(other) => {
let kind = match other {
serde_json::Value::Bool(_) => "a boolean",
serde_json::Value::Number(_) => "a number",
serde_json::Value::Array(_) => "an array",
serde_json::Value::Object(_) => "an object",
_ => "a non-string value",
};
return Err(anyhow::anyhow!(
"{webhook_key} must be a URL string, got {kind}"
));
}
}
let diff = diff_global_settings(current, desired, ApplyMode::Replace);
apply_settings_diff(db, &diff).await?;
Ok(())
}
/// Apply a settings diff to the global_settings table.
pub async fn apply_settings_diff(
db: &sqlx::Pool<sqlx::Postgres>,
diff: &SettingsDiff,
) -> anyhow::Result<()> {
for (key, value) in &diff.upserts {
sqlx::query(View on GitHub (pinned to e474e8803c)
Solutions
- Quote the value in YAML (github_app_webhook_base_url: "https://...") so it parses as a string
- Replace any non-scalar (list/map) with the intended URL string
- Re-run the sync; the fix is purely in the submitted config, no server state to repair
- Note the error intentionally hides the value — inspect your local config file to see what was actually submitted
Example fix
// before (YAML type coercion) github_app_webhook_base_url: 1234 # parsed as a number // after github_app_webhook_base_url: "https://windmill.example.com/webhook"
Defensive patterns
Strategy: validation
Validate before calling
// YAML: quote scalar values so parsers don't coerce types
github_app_webhook_base_url: "https://windmill.example.com/webhook"
// pre-flight in code:
let v = desired.get("github_app_webhook_base_url").unwrap();
assert!(v.is_string(), "github_app_webhook_base_url must be a JSON string, got kind {}", json_kind(v)); Type guard
fn json_kind(v: &serde_json::Value) -> &'static str {
match v {
serde_json::Value::String(_) => "string",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Number(_) => "number",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
serde_json::Value::Null => "null",
}
} Try / catch
match sync_global_settings_declarative(&db, ¤t, &desired).await {
Err(e) if e.to_string().contains("must be a URL string, got") =>
anyhow::bail!("your config file has a non-string value for github_app_webhook_base_url; quote it in YAML — {}", e),
other => other,
} Prevention
- Quote all URL settings in YAML to defeat bool/number coercion (on, yes, numeric strings)
- Run a JSON-schema/lint pass over declarative settings files before sync
- Remember the error hides the value by design — inspect the local config, not logs, to see what was submitted
When it happens
Trigger: A YAML/JSON config where the value is unquoted so YAML parses it as bool/number (e.g. true, 1234), or the value is a list/map instead of a scalar string, fed to sync-config or the operator's ConfigMap sync.
Common situations: YAML type coercion traps (on/off/yes become booleans, zip codes/ports become numbers), someone putting a nested object where a URL string belongs, templating errors producing a map instead of a string.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- wmill-lock.yaml is malformed (expected an object). Refusing
- Flow schema must be an object or null
- Flow JSON must be an object
- Document must be a string
- Unsupported trigger kind: ${target.triggerKind}
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/3d1c8f9df5a53f13.
Report an issue: GitHub.