[One Workflow] Add form_data support to kibana.request step#265671
[One Workflow] Add form_data support to kibana.request step#265671talboren merged 5 commits intoelastic:mainfrom
Conversation
Adds multipart/form-data upload capability to the `kibana.request` workflow step, enabling APIs that require file uploads (e.g. POST /api/saved_objects/_import) to be called from a workflow. Changes: - kibana_action_step.ts: new form_data branch in executeKibanaRequest builds a native FormData object from the step config and passes it to fetch, letting the runtime set the correct multipart boundary Content-Type automatically. Guards against body + form_data being set simultaneously. Moves Content-Type out of getAuthHeaders() and sets it explicitly only in JSON body branches. - connector_action_schema.ts: adds form_data and query fields to the kibana.request paramsSchema so the YAML editor allows the new field. Fixes method being erroneously required when the builder defaults it. Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds multipart upload support to the kibana.request workflow step by introducing a form_data parameter, while also correcting header/method schema behavior.
Changes:
- Make
methodoptional in thekibana.requestconnector schema (builder defaults toGET). - Add
form_data(multipart/form-data) andquerysupport tokibana.request. - Stop setting
Content-Type: application/jsoninsidegetAuthHeaders()and instead apply it only to JSON request paths.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/platform/plugins/shared/workflows_management/common/connector_action_schema.ts | Extends connector params schema to support optional method, query, and multipart form_data. |
| src/platform/plugins/shared/workflows_execution_engine/server/step/kibana_action_step.ts | Implements form_data handling, adjusts header construction, and builds multipart bodies for fetch requests. |
Comments suppressed due to low confidence (1)
src/platform/plugins/shared/workflows_execution_engine/server/step/kibana_action_step.ts:1
- The typing and field naming are inconsistent: the type shape includes
request.formData, but the runtime logic usescleanParams.form_data(snake_case) and later maps torequestConfig.formData. This makes the cast misleading and forces additionalascasts later. Consider aligning types with the user-facing schema (form_data) and explicitly mapping once into an internal normalized shape (e.g., normalize toformDataimmediately aftercleanParams) so you can avoid inaccurate type assertions.
/*
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const blob = new Blob([spec.content], { type: spec.content_type ?? 'text/plain' }); | ||
| fd.append(fieldName, blob, spec.filename ?? fieldName); |
There was a problem hiding this comment.
fd.append(fieldName, blob, spec.filename ?? fieldName) always supplies a filename (defaulting to the field name). That forces every part to be treated as a “file” field (Content-Disposition includes filename), which can break APIs expecting normal text form fields. Only pass the filename argument when the user provided filename, and for non-file fields consider appending a string value (or a Blob without a filename) so they serialize as regular form fields.
| const blob = new Blob([spec.content], { type: spec.content_type ?? 'text/plain' }); | |
| fd.append(fieldName, blob, spec.filename ?? fieldName); | |
| if (spec.filename !== undefined) { | |
| const blob = new Blob([spec.content], { type: spec.content_type ?? 'text/plain' }); | |
| fd.append(fieldName, blob, spec.filename); | |
| } else if (typeof spec.content === 'string') { | |
| fd.append(fieldName, spec.content); | |
| } else { | |
| const blob = new Blob([spec.content], { type: spec.content_type ?? 'text/plain' }); | |
| fd.append(fieldName, blob); | |
| } |
There was a problem hiding this comment.
TLDR impact: endpoint expecting a plain text form field may reject it or parse it incorrectly.
Example: request intent: send description=monthly sync (text) + file=@export.ndjson.
What current code does: always calls fd.append(name, blob, filename) so even description becomes a file part (filename="description") although it's a txt.
| const fd = new FormData(); | ||
| for (const [fieldName, spec] of Object.entries(formData)) { | ||
| const blob = new Blob([spec.content], { type: spec.content_type ?? 'text/plain' }); |
There was a problem hiding this comment.
This introduces runtime dependencies on global FormData and Blob in server-side code. If the Kibana server runtime doesn’t guarantee these globals (or TS libs don’t include them in this build target), this will fail at runtime or compile-time. Prefer importing FormData/Blob from the same fetch implementation used in the server (commonly undici) or using an existing Kibana/server utility for multipart encoding to ensure consistency across supported Node versions.
There was a problem hiding this comment.
Low (true check for body + form_data) so in case body is empty and form_data isn't there is no err/etc...
…step/kibana_action_step.ts Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
- Fix fd.append to only include filename when explicitly set; plain text fields are appended as strings (no Content-Disposition filename), typed blobs without a filename are appended without one. This prevents non-file fields from being treated as file parts by the server. - Extract buildFormData helper to keep makeHttpRequest complexity in check. Made-with: Cursor
💚 Build Succeeded
Metrics [docs]Async chunks
History
|
|
Starting backport for target branches: 9.4 https://github.com/elastic/kibana/actions/runs/25117834373 |
Aligns the http connector implementation with elastic#265671 (kibana.request) and removes the need to promote form-data from devDependencies. - http_connector.ts: build a native FormData with Blob parts and let axios serialize it and set the multipart Content-Type with the right boundary itself. Strip any user-supplied Content-Type in form_data mode so axios can set its own. - http_connector.test.ts: assert against the native FormData/Blob/File APIs (form.entries(), File.name/type/text()) instead of the form-data package's stream-based API. - package.json: revert the move; form-data stays in devDependencies. - kbn-data-forge/install_kibana_assets.ts: restore the eslint-disable-next-line for import/no-extraneous-dependencies on the form-data import (necessary again now that form-data is back in devDependencies). Made-with: Cursor
💚 All backports created successfully
Note: Successful backport PRs will be merged automatically after passing CI. Questions ?Please refer to the Backport tool documentation |
…65671) (#266438) # Backport This will backport the following commits from `main` to `9.4`: - [[One Workflow] Add form_data support to kibana.request step (#265671)](#265671) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Tal","email":"tal.borenstein@elastic.co"},"sourceCommit":{"committedDate":"2026-04-29T15:25:15Z","message":"[One Workflow] Add form_data support to kibana.request step (#265671)\n\n## Summary\n\n- Adds `form_data` param to the `kibana.request` workflow step, enabling\nmultipart/form-data uploads — specifically to support `POST\n/api/saved_objects/_import` (and any other Kibana API that uses `--form\nfile=@...` in curl).\n- Fixes `Content-Type: application/json` being incorrectly set inside\n`getAuthHeaders()` (an auth-unrelated header); it is now set explicitly\nonly in the JSON-body branches.\n- Fixes `method` being required in the `kibana.request` connector schema\nwhen the builder already defaults it to `GET`.\n\n## How it works\n\n```yaml\n- name: import_saved_objects\n type: kibana.request\n with:\n method: POST\n path: /api/saved_objects/_import?overwrite=true\n form_data:\n file:\n content: \"{{ inputs.ndjson }}\"\n filename: export.ndjson\n content_type: application/ndjson\n```\n\n`form_data` is mutually exclusive with `body` — using both throws a\nclear error at runtime.\n\n## Use case\n\nEnables hub-and-spoke dashboard sync across ECH clusters by exporting\nNDJSON from a source cluster and importing it into replica clusters\nentirely within a workflow — no external tooling required.\n\n\nMade with [Cursor](https://cursor.com)\n\n---------\n\nCo-authored-by: Marco Liberati <dej611@users.noreply.github.com>","sha":"37ad91dd9196752e8fbed1ebc19189cbe0282f1a","branchLabelMapping":{"^v9.5.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:version","Team:One Workflow","v9.4.0","v9.5.0"],"title":"[One Workflow] Add form_data support to kibana.request step","number":265671,"url":"https://github.com/elastic/kibana/pull/265671","mergeCommit":{"message":"[One Workflow] Add form_data support to kibana.request step (#265671)\n\n## Summary\n\n- Adds `form_data` param to the `kibana.request` workflow step, enabling\nmultipart/form-data uploads — specifically to support `POST\n/api/saved_objects/_import` (and any other Kibana API that uses `--form\nfile=@...` in curl).\n- Fixes `Content-Type: application/json` being incorrectly set inside\n`getAuthHeaders()` (an auth-unrelated header); it is now set explicitly\nonly in the JSON-body branches.\n- Fixes `method` being required in the `kibana.request` connector schema\nwhen the builder already defaults it to `GET`.\n\n## How it works\n\n```yaml\n- name: import_saved_objects\n type: kibana.request\n with:\n method: POST\n path: /api/saved_objects/_import?overwrite=true\n form_data:\n file:\n content: \"{{ inputs.ndjson }}\"\n filename: export.ndjson\n content_type: application/ndjson\n```\n\n`form_data` is mutually exclusive with `body` — using both throws a\nclear error at runtime.\n\n## Use case\n\nEnables hub-and-spoke dashboard sync across ECH clusters by exporting\nNDJSON from a source cluster and importing it into replica clusters\nentirely within a workflow — no external tooling required.\n\n\nMade with [Cursor](https://cursor.com)\n\n---------\n\nCo-authored-by: Marco Liberati <dej611@users.noreply.github.com>","sha":"37ad91dd9196752e8fbed1ebc19189cbe0282f1a"}},"sourceBranch":"main","suggestedTargetBranches":["9.4"],"targetPullRequestStates":[{"branch":"9.4","label":"v9.4.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.5.0","branchLabelMappingKey":"^v9.5.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/265671","number":265671,"mergeCommit":{"message":"[One Workflow] Add form_data support to kibana.request step (#265671)\n\n## Summary\n\n- Adds `form_data` param to the `kibana.request` workflow step, enabling\nmultipart/form-data uploads — specifically to support `POST\n/api/saved_objects/_import` (and any other Kibana API that uses `--form\nfile=@...` in curl).\n- Fixes `Content-Type: application/json` being incorrectly set inside\n`getAuthHeaders()` (an auth-unrelated header); it is now set explicitly\nonly in the JSON-body branches.\n- Fixes `method` being required in the `kibana.request` connector schema\nwhen the builder already defaults it to `GET`.\n\n## How it works\n\n```yaml\n- name: import_saved_objects\n type: kibana.request\n with:\n method: POST\n path: /api/saved_objects/_import?overwrite=true\n form_data:\n file:\n content: \"{{ inputs.ndjson }}\"\n filename: export.ndjson\n content_type: application/ndjson\n```\n\n`form_data` is mutually exclusive with `body` — using both throws a\nclear error at runtime.\n\n## Use case\n\nEnables hub-and-spoke dashboard sync across ECH clusters by exporting\nNDJSON from a source cluster and importing it into replica clusters\nentirely within a workflow — no external tooling required.\n\n\nMade with [Cursor](https://cursor.com)\n\n---------\n\nCo-authored-by: Marco Liberati <dej611@users.noreply.github.com>","sha":"37ad91dd9196752e8fbed1ebc19189cbe0282f1a"}}]}] BACKPORT--> Co-authored-by: Tal <tal.borenstein@elastic.co> Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
Summary
form_dataparam to thekibana.requestworkflow step, enabling multipart/form-data uploads — specifically to supportPOST /api/saved_objects/_import(and any other Kibana API that uses--form file=@...in curl).Content-Type: application/jsonbeing incorrectly set insidegetAuthHeaders()(an auth-unrelated header); it is now set explicitly only in the JSON-body branches.methodbeing required in thekibana.requestconnector schema when the builder already defaults it toGET.How it works
form_datais mutually exclusive withbody— using both throws a clear error at runtime.Use case
Enables hub-and-spoke dashboard sync across ECH clusters by exporting NDJSON from a source cluster and importing it into replica clusters entirely within a workflow — no external tooling required.
Made with Cursor