|
| 1 | +--- |
| 2 | +layout: default |
| 3 | +title: Workflow Queries with Formatted Data |
| 4 | +permalink: /docs/concepts/workflow-queries-formatted-data |
| 5 | +--- |
| 6 | + |
| 7 | +# Workflow Queries with Formatted Data |
| 8 | + |
| 9 | +This guide explains how to implement workflow queries that return preformatted data for enhanced rendering in Cadence Web UI. This feature allows workflow authors to return structured data in Markdown format that can be rendered directly in the Cadence Web interface, providing richer visualization and better user experience. |
| 10 | + |
| 11 | +The formatted data feature enables workflows to respond to queries with data that includes rendering instructions, allowing the UI to display content beyond simple text responses. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Overview |
| 16 | + |
| 17 | +### The Goal |
| 18 | + |
| 19 | +Support rendering preformatted data on cadence-web in places such as the Query API. Examples of data that can be preformatted include: |
| 20 | + |
| 21 | +- **Markdown** - Rich text with formatting, links, and structure |
| 22 | + |
| 23 | +The reason for prerendering is that workflow authors have access to workflow data that they may wish to render on the Cadence UI, and such rendering entirely client-side is difficult given the current Cadence workflow API. |
| 24 | + |
| 25 | +### How It Works |
| 26 | + |
| 27 | +When a workflow query responds with data in a specific shape, Cadence Web can render it with appropriate formatting. The response must include: |
| 28 | + |
| 29 | +1. A response type identifier |
| 30 | +2. A MIME type format specifier |
| 31 | +3. The actual formatted data |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +## Response Format |
| 36 | + |
| 37 | +### Basic Structure |
| 38 | + |
| 39 | +To enable formatted rendering, your workflow query must respond with data in the following shape: |
| 40 | + |
| 41 | +```json |
| 42 | +{ |
| 43 | + "cadenceResponseType": "formattedData", |
| 44 | + "format": "<mime-type format>", |
| 45 | + "data": "<formatted data specific to the format>" |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +### Supported MIME Type |
| 50 | + |
| 51 | +The `format` field should contain the supported MIME type identifier: |
| 52 | + |
| 53 | +- `text/markdown` - Markdown content |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +## Examples |
| 58 | + |
| 59 | +### Markdown Response |
| 60 | + |
| 61 | +```json |
| 62 | +{ |
| 63 | + "cadenceResponseType": "formattedData", |
| 64 | + "format": "text/markdown", |
| 65 | + "data": "### Workflow Status Report\n\n**Current Stage:** Processing\n\n- [x] Data validation completed\n- [x] Initial processing done\n- [ ] Final verification pending\n\n[View detailed logs](https://internal.example.com/logs/workflow-123)\n\n**Progress:** 75% complete" |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +This would render as: |
| 70 | + |
| 71 | +### Workflow Status Report |
| 72 | + |
| 73 | +**Current Stage:** Processing |
| 74 | + |
| 75 | +- [x] Data validation completed |
| 76 | +- [x] Initial processing done |
| 77 | +- [ ] Final verification pending |
| 78 | + |
| 79 | +[View detailed logs](https://internal.example.com/logs/workflow-123) |
| 80 | + |
| 81 | +**Progress:** 75% complete |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Go Implementation |
| 86 | + |
| 87 | +### Type Definition |
| 88 | + |
| 89 | +```go |
| 90 | +// PrerenderedQueryResponse defines the structure for formatted query responses |
| 91 | +type PrerenderedQueryResponse struct { |
| 92 | + CadenceResponseType string `json:"cadenceResponseType"` |
| 93 | + Format string `json:"format"` |
| 94 | + Data json.RawMessage `json:"data"` |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +### Example Usage |
| 99 | + |
| 100 | +```go |
| 101 | +package main |
| 102 | + |
| 103 | +import ( |
| 104 | + "context" |
| 105 | + "encoding/json" |
| 106 | + "go.uber.org/cadence/workflow" |
| 107 | +) |
| 108 | + |
| 109 | +// Example workflow with formatted query response |
| 110 | +func SampleWorkflow(ctx workflow.Context) error { |
| 111 | + // Workflow implementation... |
| 112 | + return nil |
| 113 | +} |
| 114 | + |
| 115 | +// Query handler that returns formatted markdown |
| 116 | +func WorkflowStatusQuery(ctx workflow.Context) (PrerenderedQueryResponse, error) { |
| 117 | + // Get current workflow state |
| 118 | + progress := getWorkflowProgress(ctx) |
| 119 | + |
| 120 | + // Create markdown content |
| 121 | + markdown := fmt.Sprintf(`### Workflow Status Report |
| 122 | +
|
| 123 | +**Current Stage:** %s |
| 124 | +
|
| 125 | +**Progress:** %d%% complete |
| 126 | +
|
| 127 | +**Tasks Completed:** |
| 128 | +%s |
| 129 | +
|
| 130 | +**Next Steps:** |
| 131 | +%s |
| 132 | +
|
| 133 | +--- |
| 134 | +*Last updated: %s* |
| 135 | +`, |
| 136 | + progress.CurrentStage, |
| 137 | + progress.PercentComplete, |
| 138 | + formatTaskList(progress.CompletedTasks), |
| 139 | + formatTaskList(progress.PendingTasks), |
| 140 | + time.Now().Format("2006-01-02 15:04:05"), |
| 141 | + ) |
| 142 | + |
| 143 | + return PrerenderedQueryResponse{ |
| 144 | + CadenceResponseType: "formattedData", |
| 145 | + Format: "text/markdown", |
| 146 | + Data: json.RawMessage(fmt.Sprintf(`"%s"`, markdown)), |
| 147 | + }, nil |
| 148 | +} |
| 149 | + |
| 150 | +// Helper function for creating markdown responses |
| 151 | +func NewMarkdownQueryResponse(md string) PrerenderedQueryResponse { |
| 152 | + data, _ := json.Marshal(md) |
| 153 | + return PrerenderedQueryResponse{ |
| 154 | + CadenceResponseType: "formattedData", |
| 155 | + Format: "text/markdown", |
| 156 | + Data: data, |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +// Register the query handler |
| 161 | +func init() { |
| 162 | + workflow.RegisterQueryHandler("workflow_status", WorkflowStatusQuery) |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +### Advanced Example with Error Handling |
| 167 | + |
| 168 | +```go |
| 169 | +func DetailedWorkflowQuery(ctx workflow.Context, queryType string) (interface{}, error) { |
| 170 | + switch queryType { |
| 171 | + case "status": |
| 172 | + return createStatusMarkdown(ctx) |
| 173 | + default: |
| 174 | + return nil, fmt.Errorf("unknown query type: %s", queryType) |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +func createStatusMarkdown(ctx workflow.Context) (PrerenderedQueryResponse, error) { |
| 179 | + status := getCurrentStatus(ctx) |
| 180 | + |
| 181 | + markdown := fmt.Sprintf(`# Workflow Execution Report |
| 182 | +
|
| 183 | +## Summary |
| 184 | +- **ID:** %s |
| 185 | +- **Status:** %s |
| 186 | +- **Started:** %s |
| 187 | +- **Duration:** %s |
| 188 | +
|
| 189 | +## Recent Activities |
| 190 | +%s |
| 191 | +
|
| 192 | +## Errors |
| 193 | +%s |
| 194 | +`, |
| 195 | + workflow.GetInfo(ctx).WorkflowExecution.ID, |
| 196 | + status.State, |
| 197 | + status.StartTime.Format("2006-01-02 15:04:05"), |
| 198 | + time.Since(status.StartTime).String(), |
| 199 | + formatActivities(status.Activities), |
| 200 | + formatErrors(status.Errors), |
| 201 | + ) |
| 202 | + |
| 203 | + return NewMarkdownQueryResponse(markdown), nil |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +--- |
| 208 | + |
| 209 | +## Security Considerations |
| 210 | + |
| 211 | +### XSS Prevention |
| 212 | + |
| 213 | +Taking input from a workflow and rendering it as Markdown without sanitization is a potential XSS (Cross-Site Scripting) vector. An attacker could inject malicious content including: |
| 214 | + |
| 215 | +- Raw HTML tags that might be processed by the Markdown renderer |
| 216 | +- JavaScript in HTML tags embedded within Markdown |
| 217 | +- Malicious links or images that could exfiltrate data |
| 218 | + |
| 219 | +### Mitigation Strategies |
| 220 | + |
| 221 | +1. **Server-Side Sanitization**: All content must be sanitized before rendering |
| 222 | +2. **Content Security Policy (CSP)**: Implement strict CSP headers |
| 223 | +3. **Input Validation**: Validate format types and data structure |
| 224 | +4. **Allowlist Approach**: Only allow the known-safe MIME type |
| 225 | + |
| 226 | +### Best Practices |
| 227 | + |
| 228 | +- Always validate the `cadenceResponseType` field |
| 229 | +- Sanitize all user-provided content before rendering |
| 230 | +- Use Content Security Policy headers |
| 231 | +- Regularly audit and update sanitization libraries |
| 232 | +- Consider implementing rate limiting for query requests |
| 233 | + |
| 234 | +--- |
| 235 | + |
| 236 | +## Integration with Cadence Web |
| 237 | + |
| 238 | +### Client-Side Rendering |
| 239 | + |
| 240 | +The Cadence Web UI automatically detects formatted responses and renders them appropriately: |
| 241 | + |
| 242 | +1. **Detection**: Check for `cadenceResponseType: "formattedData"` |
| 243 | +2. **Format Processing**: Parse the `format` field to determine renderer |
| 244 | +3. **Data Rendering**: Apply appropriate rendering logic based on MIME type |
| 245 | +4. **Sanitization**: Apply security sanitization before display |
| 246 | + |
| 247 | +### Supported Renderers |
| 248 | + |
| 249 | +- **Markdown**: Rendered using a markdown parser with syntax highlighting |
| 250 | + |
| 251 | +### Fallback Behavior |
| 252 | + |
| 253 | +If the formatted response cannot be rendered: |
| 254 | + |
| 255 | +1. Display the raw data as JSON |
| 256 | +2. Show an error message indicating the format issue |
| 257 | +3. Provide option to view raw response data |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +## Testing and Debugging |
| 262 | + |
| 263 | +### Testing Formatted Responses |
| 264 | + |
| 265 | +```go |
| 266 | +func TestFormattedQueryResponse(t *testing.T) { |
| 267 | + // Create test workflow environment |
| 268 | + env := testsuite.NewTestWorkflowEnvironment() |
| 269 | + |
| 270 | + // Register workflow and query |
| 271 | + env.RegisterWorkflow(SampleWorkflow) |
| 272 | + env.SetQueryHandler("status", WorkflowStatusQuery) |
| 273 | + |
| 274 | + // Start workflow |
| 275 | + env.ExecuteWorkflow(SampleWorkflow) |
| 276 | + |
| 277 | + // Query with formatted response |
| 278 | + result, err := env.QueryWorkflow("status") |
| 279 | + require.NoError(t, err) |
| 280 | + |
| 281 | + var response PrerenderedQueryResponse |
| 282 | + err = result.Get(&response) |
| 283 | + require.NoError(t, err) |
| 284 | + |
| 285 | + // Validate response structure |
| 286 | + assert.Equal(t, "formattedData", response.CadenceResponseType) |
| 287 | + assert.Equal(t, "text/markdown", response.Format) |
| 288 | + assert.NotEmpty(t, response.Data) |
| 289 | +} |
| 290 | +``` |
| 291 | +### Debugging Tips |
| 292 | + |
| 293 | +1. **Validate JSON Structure**: Ensure response matches expected format |
| 294 | +2. **Check MIME Types**: Verify format field contains valid MIME type |
| 295 | +3. **Test Sanitization**: Confirm content is properly sanitized |
| 296 | +4. **Monitor Performance**: Large formatted responses may impact query performance |
| 297 | + |
| 298 | +--- |
| 299 | + |
| 300 | +## Additional Resources |
| 301 | + |
| 302 | +### Related Documentation |
| 303 | + |
| 304 | +- [Basic Workflow Queries](/docs/concepts/queries) - Overview of standard query functionality |
| 305 | +- [Cadence Web UI Documentation](https://github.com/uber/cadence-web) - UI components and rendering |
| 306 | +- [OWASP XSS Prevention](https://owasp.org/www-community/attacks/xss/) - Security best practices |
| 307 | + |
| 308 | +### Code References |
| 309 | + |
| 310 | +- [Go Implementation Example](https://sg.uberinternal.com/code.uber.internal/uber-code/go-code/-/blob/src/code.uber.internal/cadence/operations/workflow/history-db-scan/instructions.go?L22) |
| 311 | +- [Cadence Go Client Documentation](https://pkg.go.dev/go.uber.org/cadence) |
| 312 | + |
| 313 | +### Community Resources |
| 314 | + |
| 315 | +- [Cadence Community Slack](https://join.slack.com/t/cadenceworkflow/shared_invite/enQtNDczNTgxMjYyNzlmLTJmZDlkMjNhZjRmNjhkYjdlN2I0NGQ0YzgwZGUxM2JmNWFlZTI0OTM0NDgzZTZjNTk4YWYyOGQ3YjgzNzUwNjQ) |
| 316 | +- [GitHub Discussions](https://github.com/cadence-workflow/cadence/discussions) |
0 commit comments