Skip to content

Commit 4da7461

Browse files
authored
Merge pull request cadence-workflow#287 from Bueller87/add-query-with-markdown-return-samples
feat: add Custom Workflow Controls (Markdoc query) samples
2 parents 722029e + bfad3e2 commit 4da7461

13 files changed

Lines changed: 1643 additions & 1 deletion

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ These samples demonstrate various capabilities of Java Cadence client and server
2727
the result to a destination. The first activity can be picked up by any worker. However, the second and third activities
2828
must be executed on the same host as the first one.
2929

30+
* **Custom Workflow Controls** ([`com.uber.cadence.samples.query`](src/main/java/com/uber/cadence/samples/query/)) — workflow queries that return **markdown** for Cadence Web (Markdoc buttons that **signal** workflows or **start** new workflows). **Requires Cadence Web v4.0.14+.** Copy-paste run instructions: [query samples README](src/main/java/com/uber/cadence/samples/query/README.md).
31+
3032
## Get the Samples
3133

3234
Run the following commands:
@@ -79,6 +81,8 @@ top right corner from "Open" to "Closed" to see the list of the completed workfl
7981
Click on a *RUN ID* of a workflow to see more details about it. Try different view formats to get a different level of
8082
details about the execution history.
8183

84+
For **query responses rendered as markdown** (Custom Workflow Controls), use **Cadence Web v4.0.14 or newer** and follow the [query samples README](src/main/java/com/uber/cadence/samples/query/README.md).
85+
8286
## Install Cadence CLI
8387

8488
[Command Line Interface Documentation](https://mfateev.github.io/cadence/docs/08_cli)
@@ -118,7 +122,23 @@ execute together, we recommend that you run more than one instance of this worke
118122
The second command starts workflows. Each invocation starts a new workflow execution.
119123

120124
./gradlew -q execute -PmainClass=com.uber.cadence.samples.fileprocessing.FileProcessingStarter
121-
125+
126+
### Custom Workflow Controls (Markdoc query responses)
127+
128+
These samples need **Cadence Web v4.0.14+**. Run a **worker** in one terminal, then a **starter** in another. See [src/main/java/com/uber/cadence/samples/query/README.md](src/main/java/com/uber/cadence/samples/query/README.md) for full copy-paste steps.
129+
130+
Worker (task list `query`):
131+
132+
./gradlew -q execute -PmainClass=com.uber.cadence.samples.query.QueryWorker
133+
134+
Starters (pick one per run):
135+
136+
./gradlew -q execute -PmainClass=com.uber.cadence.samples.query.MarkdownQueryStarter
137+
./gradlew -q execute -PmainClass=com.uber.cadence.samples.query.LunchVoteStarter
138+
./gradlew -q execute -PmainClass=com.uber.cadence.samples.query.OrderFulfillmentStarter
139+
140+
In Cadence Web, open the workflow → **Query** tab → run query **`Signal`**, **`options`**, or **`dashboard`** (matching the starter you used).
141+
122142
### Trip Booking
123143

124144
Cadence implementation of the [Camunda BPMN trip booking example](https://github.com/berndruecker/trip-booking-saga-java)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
7+
* use this file except in compliance with the License. A copy of the License is
8+
* located at
9+
*
10+
* http://aws.amazon.com/apache2.0
11+
*
12+
* or in the "license" file accompanying this file. This file is distributed on
13+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
14+
* express or implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*/
17+
18+
package com.uber.cadence.samples.query;
19+
20+
import com.uber.cadence.client.WorkflowClient;
21+
import com.uber.cadence.client.WorkflowOptions;
22+
import java.time.Duration;
23+
import java.util.UUID;
24+
25+
/** Starts a {@link LunchVoteWorkflow} execution. Use Cadence Web Query tab → options. */
26+
public final class LunchVoteStarter {
27+
28+
private LunchVoteStarter() {}
29+
30+
public static void main(String[] args) {
31+
try {
32+
WorkflowClient workflowClient = QuerySampleSupport.newWorkflowClient();
33+
34+
WorkflowOptions options =
35+
new WorkflowOptions.Builder()
36+
.setTaskList(QueryConstants.TASK_LIST)
37+
.setExecutionStartToCloseTimeout(Duration.ofMinutes(12))
38+
.setWorkflowId("lunch-vote-" + UUID.randomUUID())
39+
.build();
40+
41+
LunchVoteWorkflow.WorkflowIface workflow =
42+
workflowClient.newWorkflowStub(LunchVoteWorkflow.WorkflowIface.class, options);
43+
44+
// Starts the workflow asynchronously — the starter can exit immediately.
45+
WorkflowClient.start(workflow::run);
46+
System.out.println(
47+
"Started LunchVoteWorkflow. In Cadence Web (v4.0.14+), open the run, Query tab → options.");
48+
System.exit(0);
49+
} catch (RuntimeException e) {
50+
if (QuerySampleSupport.printHintIfDomainMissing(e)) {
51+
System.exit(1);
52+
}
53+
throw e;
54+
}
55+
}
56+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
7+
* use this file except in compliance with the License. A copy of the License is
8+
* located at
9+
*
10+
* http://aws.amazon.com/apache2.0
11+
*
12+
* or in the "license" file accompanying this file. This file is distributed on
13+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
14+
* express or implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*/
17+
18+
package com.uber.cadence.samples.query;
19+
20+
import static com.uber.cadence.samples.query.QueryConstants.CLUSTER;
21+
import static com.uber.cadence.samples.query.QueryConstants.DOMAIN;
22+
import static com.uber.cadence.samples.query.QueryConstants.TASK_LIST;
23+
24+
import com.uber.cadence.workflow.QueryMethod;
25+
import com.uber.cadence.workflow.SignalMethod;
26+
import com.uber.cadence.workflow.Workflow;
27+
import com.uber.cadence.workflow.WorkflowMethod;
28+
import java.time.Duration;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
32+
/**
33+
* Demonstrates interactive voting via Cadence Web (v4.0.14+). Signals accumulate votes and the
34+
* query named {@code options} renders current results and vote buttons as markdown.
35+
*/
36+
public final class LunchVoteWorkflow {
37+
38+
private LunchVoteWorkflow() {}
39+
40+
/**
41+
* Signal payload for a lunch vote. Public fields are required so Cadence's JSON data converter
42+
* can deserialize the signal input. Field names must match the JSON keys in the Markdoc
43+
* {@code input=} attribute (e.g. {@code input={"location":"Farmhouse","meal":"Red Thai Curry"}}).
44+
*/
45+
public static class LunchOrder {
46+
public String location;
47+
public String meal;
48+
public String requests;
49+
50+
/** No-arg constructor required for JSON deserialization. */
51+
public LunchOrder() {}
52+
53+
public LunchOrder(String location, String meal, String requests) {
54+
this.location = location;
55+
this.meal = meal;
56+
this.requests = requests;
57+
}
58+
}
59+
60+
/**
61+
* The voting pattern: the workflow method keeps the execution open for a voting window, signals
62+
* mutate state (accumulate votes), and the query reads that state to render the current results.
63+
*/
64+
public interface WorkflowIface {
65+
66+
@WorkflowMethod(
67+
name = QueryConstants.LUNCH_VOTE_WORKFLOW_TYPE,
68+
executionStartToCloseTimeoutSeconds = 700,
69+
taskList = TASK_LIST)
70+
void run();
71+
72+
/** Visible as "options" in the Cadence Web Query dropdown. */
73+
@QueryMethod(name = "options")
74+
MarkdownFormattedResponse optionsQuery();
75+
76+
/**
77+
* {@code name} sets the signal type string the worker listens for. It must match the
78+
* {@code signalName} attribute in the Markdoc template so Cadence Web sends the right signal.
79+
*/
80+
@SignalMethod(name = "lunch_order")
81+
void lunchOrder(LunchOrder vote);
82+
}
83+
84+
public static final class WorkflowImpl implements WorkflowIface {
85+
86+
private final List<LunchOrder> votes = new ArrayList<>();
87+
88+
private String cachedWorkflowId = "";
89+
private String cachedRunId = "";
90+
91+
@Override
92+
public void run() {
93+
cachedWorkflowId = Workflow.getWorkflowInfo().getWorkflowId();
94+
cachedRunId = Workflow.getWorkflowInfo().getRunId();
95+
// Keep the workflow open for the voting period. Votes arrive via the lunchOrder signal
96+
// while the workflow sleeps; Workflow.sleep is interruptible by signals.
97+
Workflow.sleep(Duration.ofMinutes(10));
98+
}
99+
100+
@Override
101+
public MarkdownFormattedResponse optionsQuery() {
102+
String workflowId = cachedWorkflowId;
103+
String runId = cachedRunId;
104+
String voteTable = makeLunchVoteTable(votes);
105+
String menuTable = makeLunchMenu();
106+
107+
String data =
108+
"\n## Lunch Options\n\n"
109+
+ "We're voting on where to order lunch today. Select the option you want to vote for.\n\n"
110+
+ "---\n\n"
111+
+ "### Current Votes\n\n"
112+
+ voteTable
113+
+ "\n"
114+
+ "### Menu Options\n\n"
115+
+ menuTable
116+
+ "\n\n---\n\n"
117+
+ "### Cast Your Vote\n\n"
118+
+ signalBlock(
119+
workflowId,
120+
runId,
121+
"Farmhouse - Red Thai Curry",
122+
"{\"location\":\"Farmhouse\",\"meal\":\"Red Thai Curry\",\"requests\":\"spicy\"}")
123+
+ signalBlock(
124+
workflowId,
125+
runId,
126+
"Ethiopian Wat",
127+
"{\"location\":\"Ethiopian\",\"meal\":\"Wat with Injera\",\"requests\":\"\"}")
128+
+ signalBlock(
129+
workflowId,
130+
runId,
131+
"Ler Ros - Tofu Bahn Mi",
132+
"{\"location\":\"Ler Ros\",\"meal\":\"Tofu Bahn Mi\",\"requests\":\"\"}")
133+
+ "\n{% br /%}\n\n"
134+
+ "*Vote closes when workflow times out (10 minutes)*\n\t";
135+
136+
return new MarkdownFormattedResponse(data);
137+
}
138+
139+
/** Builds a Markdoc {@code {%- signal -%}} tag. Every attribute is required for Cadence Web
140+
* to route the signal to the correct workflow execution. */
141+
private static String signalBlock(
142+
String workflowId, String runId, String label, String jsonInput) {
143+
return "{% signal \n"
144+
+ "\tsignalName=\"lunch_order\" \n"
145+
+ "\tlabel=\""
146+
+ label
147+
+ "\"\n"
148+
+ "\tdomain=\""
149+
+ DOMAIN
150+
+ "\"\n"
151+
+ "\tcluster=\""
152+
+ CLUSTER
153+
+ "\"\n"
154+
+ "\tworkflowId=\""
155+
+ workflowId
156+
+ "\"\n"
157+
+ "\trunId=\""
158+
+ runId
159+
+ "\"\n"
160+
+ "\tinput="
161+
+ jsonInput
162+
+ "\n/%}\n";
163+
}
164+
165+
@Override
166+
public void lunchOrder(LunchOrder vote) {
167+
votes.add(vote);
168+
}
169+
170+
private static String makeLunchVoteTable(List<LunchOrder> votes) {
171+
if (votes.isEmpty()) {
172+
return "| Location | Meal | Requests |\n|----------|------|----------|\n| *No votes yet* | | |\n";
173+
}
174+
StringBuilder table = new StringBuilder();
175+
table.append("| Location | Meal | Requests |\n|----------|------|----------|\n");
176+
for (LunchOrder vote : votes) {
177+
table
178+
.append("| ")
179+
.append(vote.location != null ? vote.location : "")
180+
.append(" | ")
181+
.append(vote.meal != null ? vote.meal : "")
182+
.append(" | ")
183+
.append(vote.requests != null ? vote.requests : "")
184+
.append(" |\n");
185+
}
186+
return table.toString();
187+
}
188+
189+
private static String makeLunchMenu() {
190+
return "| Picture | Description |\n"
191+
+ "|---------|-------------|\n"
192+
+ "| {% image src=\"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Red_roast_duck_curry.jpg/200px-Red_roast_duck_curry.jpg\" alt=\"Red Thai Curry\" width=\"200\" /%} | **Farmhouse - Red Thai Curry**: A dish in Thai cuisine made from curry paste, coconut milk, meat, seafood, vegetables, and herbs. |\n"
193+
+ "| {% image src=\"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\" alt=\"Tofu Bahn Mi\" width=\"200\" /%} | **Ler Ros - Tofu Bahn Mi**: A Vietnamese sandwich with a baguette filled with lemongrass tofu, vegetables, and fresh herbs. |\n"
194+
+ "| {% image src=\"https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Ethiopian_wat.jpg/960px-Ethiopian_wat.jpg\" alt=\"Ethiopian Wat\" width=\"200\" /%} | **Ethiopian Wat**: A traditional Ethiopian stew made from spices, vegetables, and legumes, served with injera flatbread. |\n"
195+
+ "\n*(source: wikipedia)*";
196+
}
197+
}
198+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
7+
* use this file except in compliance with the License. A copy of the License is
8+
* located at
9+
*
10+
* http://aws.amazon.com/apache2.0
11+
*
12+
* or in the "license" file accompanying this file. This file is distributed on
13+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
14+
* express or implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*/
17+
18+
package com.uber.cadence.samples.query;
19+
20+
import com.fasterxml.jackson.annotation.JsonCreator;
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
23+
/**
24+
* The JSON shape Cadence Web (v4.0.14+) expects for formatted query responses. When a {@link
25+
* com.uber.cadence.workflow.QueryMethod} returns this object, the default data converter serializes
26+
* it to:
27+
*
28+
* <pre>{@code
29+
* {
30+
* "cadenceResponseType": "formattedData",
31+
* "format": "text/markdown",
32+
* "data": "<your markdown string>"
33+
* }
34+
* }</pre>
35+
*
36+
* Cadence Web detects this shape and renders the {@code data} field as markdown (with Markdoc
37+
* extensions) instead of displaying raw JSON.
38+
*/
39+
public final class MarkdownFormattedResponse {
40+
41+
private final String cadenceResponseType;
42+
private final String format;
43+
private final String data;
44+
45+
/**
46+
* Constructor for JSON deserialization (e.g. Java clients using the Cadence SDK data converter).
47+
* A no-arg constructor cannot populate {@code private final} fields; {@code @JsonCreator} with
48+
* explicit properties is required.
49+
*/
50+
@JsonCreator
51+
private MarkdownFormattedResponse(
52+
@JsonProperty("cadenceResponseType") String cadenceResponseType,
53+
@JsonProperty("format") String format,
54+
@JsonProperty("data") String data) {
55+
this.cadenceResponseType = cadenceResponseType;
56+
this.format = format;
57+
this.data = data;
58+
}
59+
60+
/**
61+
* @param markdownData the markdown string to render in Cadence Web. May include Markdoc tags such
62+
* as {@code {%- signal -%}} and {@code {%- start -%}}.
63+
*/
64+
public MarkdownFormattedResponse(String markdownData) {
65+
// These two values are the required constants that Cadence Web checks for.
66+
this("formattedData", "text/markdown", markdownData);
67+
}
68+
69+
public String getCadenceResponseType() {
70+
return cadenceResponseType;
71+
}
72+
73+
public String getFormat() {
74+
return format;
75+
}
76+
77+
public String getData() {
78+
return data;
79+
}
80+
}

0 commit comments

Comments
 (0)