Skip to content

[One Workflow] Add form_data support to kibana.request step#265671

Merged
talboren merged 5 commits intoelastic:mainfrom
talboren:feat/kibana-request-form-data-support
Apr 29, 2026
Merged

[One Workflow] Add form_data support to kibana.request step#265671
talboren merged 5 commits intoelastic:mainfrom
talboren:feat/kibana-request-form-data-support

Conversation

@talboren
Copy link
Copy Markdown
Contributor

@talboren talboren commented Apr 26, 2026

Summary

  • Adds form_data param to the kibana.request workflow step, enabling multipart/form-data uploads — specifically to support POST /api/saved_objects/_import (and any other Kibana API that uses --form file=@... in curl).
  • Fixes Content-Type: application/json being incorrectly set inside getAuthHeaders() (an auth-unrelated header); it is now set explicitly only in the JSON-body branches.
  • Fixes method being required in the kibana.request connector schema when the builder already defaults it to GET.

How it works

- name: import_saved_objects
  type: kibana.request
  with:
    method: POST
    path: /api/saved_objects/_import?overwrite=true
    form_data:
      file:
        content: "{{ inputs.ndjson }}"
        filename: export.ndjson
        content_type: application/ndjson

form_data is mutually exclusive with body — 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

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
@talboren talboren added release_note:enhancement backport:version Backport to applied version labels Team:One Workflow Team label for One Workflow (Workflow automation) v9.3.0 v9.4.0 release_note:skip Skip the PR/issue when compiling release notes and removed v9.3.0 release_note:enhancement labels Apr 26, 2026
@talboren talboren marked this pull request as ready for review April 26, 2026 09:37
@talboren talboren requested a review from a team as a code owner April 26, 2026 09:37
@h88 h88 requested a review from Copilot April 28, 2026 08:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 method optional in the kibana.request connector schema (builder defaults to GET).
  • Add form_data (multipart/form-data) and query support to kibana.request.
  • Stop setting Content-Type: application/json inside getAuthHeaders() 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 uses cleanParams.form_data (snake_case) and later maps to requestConfig.formData. This makes the cast misleading and forces additional as casts later. Consider aligning types with the user-facing schema (form_data) and explicitly mapping once into an internal normalized shape (e.g., normalize to formData immediately after cleanParams) so you can avoid inaccurate type assertions.
/*

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +301 to +302
const blob = new Blob([spec.content], { type: spec.content_type ?? 'text/plain' });
fd.append(fieldName, blob, spec.filename ?? fieldName);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +299 to +301
const fd = new FormData();
for (const [fieldName, spec] of Object.entries(formData)) {
const blob = new Blob([spec.content], { type: spec.content_type ?? 'text/plain' });
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low (true check for body + form_data) so in case body is empty and form_data isn't there is no err/etc...

Copy link
Copy Markdown
Contributor

@h88 h88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • If the intended scope is file-only form (by design) you may ignore the 1st comment related to reusable multipart support across endpoints (mixed content)
  • consider adding a test to guard against regressions

talboren and others added 3 commits April 29, 2026 12:05
…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
@kibanamachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
workflowsManagement 2.3MB 2.3MB +528.0B

History

@talboren talboren merged commit 37ad91d into elastic:main Apr 29, 2026
25 checks passed
@kibanamachine
Copy link
Copy Markdown
Contributor

Starting backport for target branches: 9.4

https://github.com/elastic/kibana/actions/runs/25117834373

semd added a commit to semd/kibana that referenced this pull request Apr 29, 2026
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
@kibanamachine
Copy link
Copy Markdown
Contributor

💚 All backports created successfully

Status Branch Result
9.4

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

kibanamachine added a commit that referenced this pull request Apr 29, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:version Backport to applied version labels release_note:skip Skip the PR/issue when compiling release notes Team:One Workflow Team label for One Workflow (Workflow automation) v9.4.0 v9.5.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants