Skip to content

feat: experimental: Interactivity for queries#1025

Merged
davidporter-id-au merged 17 commits intomasterfrom
feature/adding-interactivity-for-query-responses
Sep 26, 2025
Merged

feat: experimental: Interactivity for queries#1025
davidporter-id-au merged 17 commits intomasterfrom
feature/adding-interactivity-for-query-responses

Conversation

@davidporter-id-au
Copy link
Member

@davidporter-id-au davidporter-id-au commented Sep 15, 2025

Screenshot 2025-09-14 at 8 20 50 PM

This is an experimental feature or adding something very similar to slack's Block kit, to queries to allow for a degree of interactivity and sending of signals. It should allow users to specify an arbitrary markdown and response and also buttons with some signals to the workflows.

This is a draft only. I've not finished responding to all the comments (particularly Assem's)

Rough spec:

This is a small RFC about extending the preformatted query setup available in Cadence, which renders the workflow Query response in the UI as markdown with a small bit more capability: UI interactivity.

The key idea, first pointed out by Taylan Isikdemir , is to shamelessly steal to be inspired by Slack’s block-kit that a workflow could render a query response with some buttons, giving the user a choice for how to interact.

Example:

The buttons would signal the workflow, allowing the backend to then continue doing whatever user-specified behaviour. It would likely need a small amount of UI state to capture that the button had been clicked and that the signal had completed.

From the point of view of the workflow author, they would be looking to create a query with a response like this (to render the above example):

{
   "cadenceResponseType": "formattedData",
   "format": "blocks",
   "blocks": [
		{
			"type": "section",
                    "format": "text/markdown",
                    "componentOptions": {
				"text": "### Lunch options\n\n|   |    |\n|---|----|\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Red_roast_duck_curry.jpg/200px-Red_roast_duck_curry.jpg) | <b>Farmhouse - Red Thai Curry</h5> </b>: The base Thai red curry paste ... |\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png/200px-B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png) | <b>Ler Ros: Lemongrass Tofu Bahn Mi</b> In Vietnamese cuisine, bánh mì, bánh mỳ or banh mi ... |\n\n\n\n\n"
      }
			}
		},
		{
"type": "divider"
		},
		{
			"type": "actions",
			"elements": [
				{
					"type": "button",
					"componentOptions": {
						"type": "plain_text",
						"text": "Farmhouse"
					},
                                 "action": {
                                     "type": "signal",
                                     "signal_name": "lunch_order",
					    "signal_value": { "location": "farmhouse - red thai curry", "requests": "spicy" }
                                 }
					
				},
				{
					"type": "button",
					"componentOptions": {
						"type": "plain_text",
						"text": "Kin Khao"
					},
                                 "action": {
						"type": "signal",
"signal_name": "no_lunch_order_walk_in_person",
                               	       "workflow_id": "some-other-workflow",
                                        "run_id": "49ea9236-0903-420a-8c26-09bbeeb6c50f"
                                  }
					
				},
				{
					"type": "button",
					"componentOptions": {
						"type": "plain_text",
						"text": "Ler Ros"	
					},
                                 "action": {
                                     "type": "signal",
                                      "signal_name": "lunch_order",
					     "signal_value": { "location": "Ler Ros", "meal": "tofo Bahn Mi"}
                                 }
				}
			]
		}
	]
}

Implementation and details

Top level
The top level has a section called ‘blocks’ which must always be an array/list of arbitrary length. Ie:

{
   "cadenceResponseType": "formattedData",
   "format": "blocks",
   "blocks": [...]
}

Where possible values for the type blocks are: section, divider and actions. They are CSS ‘block’ elements so they take up the entire div. Contents within typically are inline.

Section
This is identical to the earlier proposal in that it is intended to be a formatted component with the intent of it being markdown, csv, svg etc (whatever existing components are supported).

{
  "type": "section",
  "format": "text/markdown",
  "componentOptions": {
 	 "text": "### Lunch options\n\n|   |    |\n|---|----|\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Red_roast_duck_curry.jpg/200px-Red_roast_duck_curry.jpg) | <b>Farmhouse - Red Thai Curry</h5> </b>: The base Thai red curry paste ... |\n| ![food](https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png/200px-B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png) | <b>Ler Ros: Lemongrass Tofu Bahn Mi</b> In Vietnamese cuisine, bánh mì, bánh mỳ or banh mi ... |\n\n\n\n\n"
  }
}

Divider
This inserts a <br /> or similar horizontal line for the aesthetic purpose of dividing content

{
  "type": "divider"
}

Actions
The actions section is an array of inputs, for now only buttons are proposed:

{
			"type": "actions",
			"elements": [ ... ]
}

And each button is proposed with the following set of fields

{
  "type": "button",
  "componentOptions": {
    "type": "<TBA>",              // Baseweb button states such as highlighted or greyed out (design input needed here)
    "text": "Ler Ros"  
  },
  "action": {
     "type": "signal"                     // an identifyier of the type of action (other types might be added later)
     "signal_name": "lunch_order",        // the signal name
     "signal_value": {                    // the signal payload
        "location": "Ler Ros",
        "meal": "tofo Bahn Mi"
  },
  "workflow_id": "some-other-workflow",
  "run_id": "49ea9236-0903-420a-8c26-09bbeeb6c50f"
  }
}

For each button, in addition to the basic html fields (text etc) they would use additional data fields signal and signal_value, stored as props to the component which correspond to their signal API components. The user may optionally wish to specify the workflow/runID for the signal (TBA).

Clicking the button would trigger a HTTP POST to the web backend using the existing signal API

Non-goals:

The buttons themselves should not allow for:

  • Any form submission
  • Any javascript invocation or onclick triggers

Security and alternatives considered

Allowing arbitrary html in markdown:

This is really quite unsafe due to the (high) risk that a workflow might run some input and unintentionally cause XSS attacks to occur. Ie, a user might not intend to have their markdown rendering trigger a http request to a hostile site, but it might occur if we were to turn off the security in markdown rendering it’d be quite easy for a workflow input getting rendered to send a HTTP request to a hostile site, allowing for things such as cookies or local-storage tokens to get stolen.

davidporter-id-au and others added 10 commits September 14, 2025 20:21
- Fix prettier formatting in blocks.styles.ts, blocks.tsx, and blocks.types.tsx
- Remove console statements from blocks.tsx
- Fix unused variable warning in error handling
- Fix import order in workflow-queries-result.types.ts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@davidporter-id-au davidporter-id-au marked this pull request as ready for review September 19, 2025 04:47
@davidporter-id-au davidporter-id-au changed the title WIP first cut for interactivity for queries feat: experimental: Interactivity for queries Sep 19, 2025
davidporter-id-au and others added 2 commits September 23, 2025 11:55
Signed-off-by: Assem Hafez <assem.hafez@uber.com>
const textToCopy = useMemo(() => {
return losslessJsonStringify(content, null, '\t');
}, [content]);
if (queryResultContent.contentType === 'blocks') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this condition ?

// Only handle signal actions
if (button.action.type !== 'signal') {
// eslint-disable-next-line no-console
console.warn(
Copy link
Contributor

Choose a reason for hiding this comment

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

Better disable or show error banner/snackbar on UI

Copy link
Member Author

Choose a reason for hiding this comment

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

ah thank you, i didn't know that was a thing


if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to signal workflow');
Copy link
Contributor

Choose a reason for hiding this comment

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

Show message as snackbar for the user

@Assem-Uber Assem-Uber requested a review from Copilot September 26, 2025 19:06
Copy link
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

This PR introduces experimental support for interactive queries in Cadence workflows by implementing a block-based UI system similar to Slack's Block Kit. The feature allows workflows to render query responses with interactive elements like buttons that can send signals back to workflows.

  • Adds a new blocks content type for query responses alongside existing json and markdown types
  • Implements a block system with support for sections, dividers, and interactive button actions
  • Enables button clicks to trigger workflow signals with customizable payloads

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/views/workflow-queries/workflow-queries.tsx Passes workflow parameters (domain, cluster, workflowId, runId) to query result component
src/views/workflow-queries/workflow-queries-result/workflow-queries-result.types.ts Extends types to support blocks content type and adds workflow parameters
src/views/workflow-queries/workflow-queries-result/workflow-queries-result.tsx Adds rendering logic for blocks content type using new Blocks component
src/views/workflow-queries/workflow-queries-result/helpers/get-query-result-content.ts Implements content parsing logic for blocks format with cadenceResponseType validation
src/views/workflow-queries/workflow-queries-result/helpers/__tests__/get-query-result-content.test.ts Adds comprehensive test coverage for blocks content type parsing
src/views/workflow-queries/workflow-queries-result/__tests__/workflow-queries-result-json.test.tsx Updates tests to handle new workflow parameters and blocks rendering
src/views/workflow-queries/__tests__/workflow-queries.test.tsx Updates mock to verify workflow parameters are passed correctly
src/components/blocks/blocks.types.tsx Defines TypeScript interfaces for block components and button actions
src/components/blocks/blocks.tsx Implements the main Blocks component with signal handling and UI rendering
src/components/blocks/blocks.styles.ts Provides styled components for block layout and visual presentation

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

<styled.ActionsContainer>
{actions.elements.map((element, index) => {
if (element.type === 'button') {
const buttonKey = `${element.action.type}-${element.action.signal_name}-${index}`;
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

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

The buttonKey generation logic is inconsistent between lines 41 and 125. Line 41 uses ${button.action.signal_name}-${index} while line 125 uses ${element.action.type}-${element.action.signal_name}-${index}. This will cause the loading state check to fail since the keys don't match.

Suggested change
const buttonKey = `${element.action.type}-${element.action.signal_name}-${index}`;
const buttonKey = `${element.action.signal_name}-${index}`;

Copilot uses AI. Check for mistakes.
<styled.ActionsContainer>
{actions.elements.map((element, index) => {
if (element.type === 'button') {
const buttonKey = `${element.action.type}-${element.action.signal_name}-${index}`;
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

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

This buttonKey should match the one used in handleButtonClick (line 41). Change this to ${element.action.signal_name}-${index} to maintain consistency.

Suggested change
const buttonKey = `${element.action.type}-${element.action.signal_name}-${index}`;
const buttonKey = `${element.action.signal_name}-${index}`;

Copilot uses AI. Check for mistakes.
@davidporter-id-au davidporter-id-au merged commit 7374138 into master Sep 26, 2025
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants