Skip to content

Commit c7b7375

Browse files
committed
feat: add Custom Workflow Controls (Markdoc query) samples
Add three workflow samples demonstrating Cadence Web v4.0.14+ Custom Workflow Controls, where query responses return interactive markdown rendered via Markdoc: - MarkdownQueryWorkflow: signal/start buttons with a wait-activity loop - LunchVoteWorkflow: interactive voting with signal-accumulated state - OrderFulfillmentWorkflow: state-machine ops dashboard with context-sensitive action buttons Signed-off-by: “Kevin” <kevlar_ksb@yahoo.com>
1 parent 25b69fa commit c7b7375

13 files changed

Lines changed: 1640 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: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
/** Cached on the workflow thread; queries must not call {@link Workflow#getWorkflowInfo()}. */
89+
private String cachedWorkflowId = "";
90+
91+
private String cachedRunId = "";
92+
93+
@Override
94+
public void run() {
95+
cachedWorkflowId = Workflow.getWorkflowInfo().getWorkflowId();
96+
cachedRunId = Workflow.getWorkflowInfo().getRunId();
97+
// Keep the workflow open for the voting period. Votes arrive via the lunchOrder signal
98+
// while the workflow sleeps; Workflow.sleep is interruptible by signals.
99+
Workflow.sleep(Duration.ofMinutes(10));
100+
}
101+
102+
@Override
103+
public MarkdownFormattedResponse optionsQuery() {
104+
String workflowId = cachedWorkflowId;
105+
String runId = cachedRunId;
106+
String voteTable = makeLunchVoteTable(votes);
107+
String menuTable = makeLunchMenu();
108+
109+
String data =
110+
"\n## Lunch Options\n\n"
111+
+ "We're voting on where to order lunch today. Select the option you want to vote for.\n\n"
112+
+ "---\n\n"
113+
+ "### Current Votes\n\n"
114+
+ voteTable
115+
+ "\n"
116+
+ "### Menu Options\n\n"
117+
+ menuTable
118+
+ "\n\n---\n\n"
119+
+ "### Cast Your Vote\n\n"
120+
+ signalBlock(
121+
workflowId,
122+
runId,
123+
"Farmhouse - Red Thai Curry",
124+
"{\"location\":\"Farmhouse\",\"meal\":\"Red Thai Curry\",\"requests\":\"spicy\"}")
125+
+ signalBlock(
126+
workflowId,
127+
runId,
128+
"Ethiopian Wat",
129+
"{\"location\":\"Ethiopian\",\"meal\":\"Wat with Injera\",\"requests\":\"\"}")
130+
+ signalBlock(
131+
workflowId,
132+
runId,
133+
"Ler Ros - Tofu Bahn Mi",
134+
"{\"location\":\"Ler Ros\",\"meal\":\"Tofu Bahn Mi\",\"requests\":\"\"}")
135+
+ "\n{% br /%}\n\n"
136+
+ "*Vote closes when workflow times out (10 minutes)*\n\t";
137+
138+
return new MarkdownFormattedResponse(data);
139+
}
140+
141+
/** Builds a Markdoc {@code {%- signal -%}} tag. Every attribute is required for Cadence Web
142+
* to route the signal to the correct workflow execution. */
143+
private static String signalBlock(
144+
String workflowId, String runId, String label, String jsonInput) {
145+
return "{% signal \n"
146+
+ "\tsignalName=\"lunch_order\" \n"
147+
+ "\tlabel=\""
148+
+ label
149+
+ "\"\n"
150+
+ "\tdomain=\""
151+
+ DOMAIN
152+
+ "\"\n"
153+
+ "\tcluster=\""
154+
+ CLUSTER
155+
+ "\"\n"
156+
+ "\tworkflowId=\""
157+
+ workflowId
158+
+ "\"\n"
159+
+ "\trunId=\""
160+
+ runId
161+
+ "\"\n"
162+
+ "\tinput="
163+
+ jsonInput
164+
+ "\n/%}\n";
165+
}
166+
167+
@Override
168+
public void lunchOrder(LunchOrder vote) {
169+
votes.add(vote);
170+
}
171+
172+
private static String makeLunchVoteTable(List<LunchOrder> votes) {
173+
if (votes.isEmpty()) {
174+
return "| Location | Meal | Requests |\n|----------|------|----------|\n| *No votes yet* | | |\n";
175+
}
176+
StringBuilder table = new StringBuilder();
177+
table.append("| Location | Meal | Requests |\n|----------|------|----------|\n");
178+
for (LunchOrder vote : votes) {
179+
table
180+
.append("| ")
181+
.append(vote.location != null ? vote.location : "")
182+
.append(" | ")
183+
.append(vote.meal != null ? vote.meal : "")
184+
.append(" | ")
185+
.append(vote.requests != null ? vote.requests : "")
186+
.append(" |\n");
187+
}
188+
return table.toString();
189+
}
190+
191+
private static String makeLunchMenu() {
192+
return "| Picture | Description |\n"
193+
+ "|---------|-------------|\n"
194+
+ "| {% 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"
195+
+ "| {% 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"
196+
+ "| {% 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"
197+
+ "\n*(source: wikipedia)*";
198+
}
199+
}
200+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
/**
21+
* The JSON shape Cadence Web (v4.0.14+) expects for formatted query responses. When a {@link
22+
* com.uber.cadence.workflow.QueryMethod} returns this object, the default data converter serializes
23+
* it to:
24+
*
25+
* <pre>{@code
26+
* {
27+
* "cadenceResponseType": "formattedData",
28+
* "format": "text/markdown",
29+
* "data": "<your markdown string>"
30+
* }
31+
* }</pre>
32+
*
33+
* Cadence Web detects this shape and renders the {@code data} field as markdown (with Markdoc
34+
* extensions) instead of displaying raw JSON.
35+
*/
36+
public final class MarkdownFormattedResponse {
37+
38+
private final String cadenceResponseType;
39+
private final String format;
40+
private final String data;
41+
42+
/**
43+
* @param markdownData the markdown string to render in Cadence Web. May include Markdoc tags such
44+
* as {@code {%- signal -%}} and {@code {%- start -%}}.
45+
*/
46+
public MarkdownFormattedResponse(String markdownData) {
47+
// These two values are the required constants that Cadence Web checks for.
48+
this.cadenceResponseType = "formattedData";
49+
this.format = "text/markdown";
50+
this.data = markdownData;
51+
}
52+
53+
public String getCadenceResponseType() {
54+
return cadenceResponseType;
55+
}
56+
57+
public String getFormat() {
58+
return format;
59+
}
60+
61+
public String getData() {
62+
return data;
63+
}
64+
}

0 commit comments

Comments
 (0)