The old code iterated every IEnumerable, so `output = "hello"` emitted one
item per character (a string is IEnumerable-of-char) and `output = {"a":1}`
emitted the keys as items. Now the script's output is serialized first and
the ValueKind decides: JSON array → per-element items; anything else →
single item. `output = None` remains an empty emission.
New tests cover string, dict, number, and None shapes.
Backend: PythonTransformNode wraps script execution in a try/catch that
runs the exception through IronPython's ExceptionOperations.FormatException.
Users now see a proper Python-style traceback (file, line, type, message)
instead of the bare CLR message (which was often empty or just "unexpected
token"). Source is named "script" for readable File lines in the trace.
Frontend: findLatestIo also returns status + error; NodeEditorDialog
renders a red-bordered traceback block in the Output column when the
node failed, replacing the misleading "No data yet" state.
Every action/terminal node's config string fields are now run through
TemplateEngine.Render before the node sees them, so operators can pull
values from the incoming item into any config field.
- {input}, {input.a.b}, {input.tags[0]}, {input["weird.key"]}
- {{ / }} escape to literal braces
- Missing paths / null input → empty string (no 500s on absent query params)
- Unrecognised / malformed placeholders pass through so typos are visible
- Strings render raw; numbers/bools stringified; objects/arrays compact JSON
Interpolation runs executor-side (Executor.RunNodeAsync walks ConfigJson
and rewrites string leaves), so every node opts in for free. 20 new tests
(17 TemplateEngine + 2 Executor + 1 end-to-end webhook→WebResponse).
POST behaviour unchanged (JSON body). GET on the same URL now triggers the
workflow with the query string collapsed into a JSON object: /wh/wf/hi?a=1&name=x
becomes {"a":"1","name":"x"}, and repeated keys collapse into a string
array (?tag=a&tag=b → {"tag":["a","b"]}). No query string yields {} —
matches the empty-POST-body semantics.
HEAD returns 200 empty so Slack/Discord link previews don't fire the workflow
AND don't flag the URL as broken. Other verbs (PUT/PATCH/DELETE/OPTIONS) still
return 405 JSON with Allow: POST, GET.
Frontend WebhookUrlHint now advertises both verbs and drops the POST prefix
from the displayed URL so the raw URL is copy-and-clickable.
6 new xUnit tests cover: GET-unknown-404, GET-with-query, GET-repeated-keys,
GET-empty-query, HEAD-200, PUT-still-405 (updated Allow header).
Backend: POST /api/connections/{id}/test decrypts the stored config, calls
provider.OpenAsync with a 10s CancellationTokenSource, closes the returned
handle in a finally, and returns {ok: bool, error?: string}. Always 200
unless the id is unknown (404) — the outcome of the round-trip belongs in
the body, not the status. 2 new xUnit tests cover the 404 path and the
"connection refused" failure path (points at 127.0.0.1:1).
Frontend: Plug icon on each ConnectionsPage row. Click → animated icon
during the round-trip, then an inline pill: 'Connected' (green check) on
success or the truncated error (red X, full text on hover) on failure.
Auto-clears after 6s. Row state is tracked per-id in a plain useState;
the shared react-query cache stays untouched. 1 new Vitest covers both
outcomes via MSW handlers.
Previously a browser click on the webhook URL fell through the POST-only
handler to MapFallbackToFile('index.html'), landing the user on the
workflows page and looking like a bug. Now non-POST verbs on the same
route return 405 with Allow: POST — GETs render a small HTML page showing
the correct curl invocation, other methods get JSON. Two new xUnit tests
cover the HTML and JSON branches and assert we don't hand back the SPA.
Backend: PUT /api/connections/{id} with UpdateConnectionRequest {Name, Config}.
Provider is immutable; a secret field left blank in the incoming config is
merged from the stored row so users can rename or rotate non-secret fields
without re-typing every credential. Invalidates the ConnectionInstanceRegistry
cache on update so the next GetConnection() re-opens with the fresh config.
409 on unique-name-per-provider collision, 404 on unknown id, 400 on non-
object config. 6 new xUnit tests cover the merge, overwrite, 404, 409, 400
paths plus the happy-path round-trip.
Frontend: ConnectionsPage gains an Edit pencil per row. The form is shared
between create and edit modes; provider select is disabled in edit; secret
fields render blank with an inline hint that leaving them blank keeps the
stored value. One new Vitest covers the edit flow.