Skip to main content

Custom Workflow Controls

Introduction

Your workflow has internal state. What if your users could see it and act on it, directly from Cadence Web, without building a separate admin panel?

Dashboard rendered in Cadence Web
View the markdown source that produces this dashboard
### ⚡ Available Actions

**Payment Review:**

{% signal
signalName="approve_payment"
label="✓ Approve Payment"
domain="cadence-samples"
cluster="cluster0"
workflowId="a8decc9c-8248-498d-b5f4-5dcc6f8ba620"
runId="5942783c-beca-4305-98c3-b7bc247d053c"
input={"operator":"ops-user"}
/%}
{% signal
signalName="reject_payment"
label="✗ Reject: Policy Violation"
domain="cadence-samples"
cluster="cluster0"
workflowId="a8decc9c-8248-498d-b5f4-5dcc6f8ba620"
runId="5942783c-beca-4305-98c3-b7bc247d053c"
input={"reason":"Policy Violation","operator":"ops-user"}
/%}
{% signal
signalName="reject_payment"
label="✗ Reject: Fraud Suspected"
domain="cadence-samples"
cluster="cluster0"
workflowId="a8decc9c-8248-498d-b5f4-5dcc6f8ba620"
runId="5942783c-beca-4305-98c3-b7bc247d053c"
input={"reason":"Fraud Suspected","operator":"ops-user"}
/%}
{% signal
signalName="reject_payment"
label="✗ Reject: Customer Request"
domain="cadence-samples"
cluster="cluster0"
workflowId="a8decc9c-8248-498d-b5f4-5dcc6f8ba620"
runId="5942783c-beca-4305-98c3-b7bc247d053c"
input={"reason":"Customer Request","operator":"ops-user"}
/%}

Cadence Web renders markdown returned by workflow queries. Add three Markdoc tags ({% signal %}, {% start %}, {% image %}) and your query response becomes a live ops dashboard. Below, we'll build one from scratch in three steps. See the full Tag Reference for all attributes.


How It Works

  1. The user runs a query from the Cadence Web Queries tab
  2. Your workflow returns markdown with MarkDoc tags in a special JSON shape
  3. Cadence Web renders it: tables, text, images, and action buttons
  4. The user clicks a button → Cadence Web sends a signal or starts a new workflow
  5. The workflow state updates → next query shows the new state

Response Format

Your query handler returns an object with this shape:

{
"cadenceResponseType": "formattedData",
"format": "text/markdown",
"data": "## Your markdown here\n\n{% signal signalName=\"approve\" ... /%}"
}
FieldValue
cadenceResponseTypeMust be "formattedData"
formatMust be "text/markdown"
dataYour markdown string (supports MarkDoc tags)

Level 1: Return Markdown

The simplest case: return a markdown string from your query handler. Cadence Web renders it as formatted text instead of raw JSON.

type markdownFormattedResponse struct {
CadenceResponseType string `json:"cadenceResponseType"`
Format string `json:"format"`
Data string `json:"data"`
}

workflow.SetQueryHandler(ctx, "status", func() (markdownFormattedResponse, error) {
md := fmt.Sprintf("## Order Status\n\n**Customer:** %s\n\n| Item | Qty |\n|------|-----|\n| %s | %d |",
order.CustomerName, order.Items[0].Name, order.Items[0].Quantity)

return markdownFormattedResponse{
CadenceResponseType: "formattedData",
Format: "text/markdown",
Data: md,
}, nil
})

Level 2: Beyond Plain Text

Signal and Start Buttons

Add {% signal %} and {% start %} tags to your markdown string. Cadence Web renders them as buttons that send real signals or start new workflows, without switching to the Cadence CLI or writing a separate client.

## My Workflow

Click to approve this order:

{% signal
signalName="approve_payment"
label="Approve Payment"
domain="my-domain"
cluster="my-cluster"
workflowId="order-123"
runId="run-456"
input={"operator":"ops-user"}
/%}

Or start a fresh workflow:

{% start
workflowType="MyWorkflow"
label="Start New Order"
domain="my-domain"
cluster="my-cluster"
taskList="my-task-list"
timeoutSeconds=3600
/%}
View the rendered result in Cadence Web

Signal and start buttons rendered as clickable controls in Cadence Web

Sized Images

Standard markdown images (![alt](url)) work, but offer no size control. The {% image %} tag lets you set width and height. It also exists because Cadence Web strips raw HTML from query responses to prevent XSS, so <img> tags in your markdown will not render.

{% image src="https://cadenceworkflow.io/img/cadence-logo.svg" alt="Cadence Logo" width="200" /%}
View the rendered result in Cadence Web
Cadence Logo

Level 3: State-Driven Dashboards

The real power: change which buttons appear based on your workflow's current state.

The OrderFulfillmentWorkflow sample demonstrates this. An order moves through states, and the dashboard shows only the actions valid for the current state:

StateAvailable Actions
Pending PaymentApprove Payment, Reject (Policy / Fraud / Customer Request)
Payment ApprovedMark Ready to Ship, Full Refund, Partial Refund
Ready to ShipShip via UPS / FedEx / USPS, Cancel Order
ShippedMark as Delivered
Delivered / Cancelled / RefundedNo actions, order complete

The query handler builds the button list dynamically based on order.Status:

func makeActionButtons(ctx workflow.Context, order Order) string {
wfID := workflow.GetInfo(ctx).WorkflowExecution.ID
runID := workflow.GetInfo(ctx).WorkflowExecution.RunID

switch order.Status {
case StatusPendingPayment:
return fmt.Sprintf(`
{%% signal signalName="approve_payment" label="Approve Payment"
domain="cadence-samples" cluster="cluster0"
workflowId="%s" runId="%s" input={"operator":"ops-user"} /%%}
{%% signal signalName="reject_payment" label="Reject: Fraud Suspected"
domain="cadence-samples" cluster="cluster0"
workflowId="%s" runId="%s" input={"reason":"Fraud Suspected","operator":"ops-user"} /%%}
`, wfID, runID, wfID, runID)

case StatusReadyToShip:
return fmt.Sprintf(`
{%% signal signalName="ship_order" label="Ship via UPS"
domain="cadence-samples" cluster="cluster0"
workflowId="%s" runId="%s"
input={"trackingNumber":"1Z999AA10123456784","carrier":"UPS","operator":"ops-user"} /%%}
`, wfID, runID)
// ... other states
}
return "*No actions available*"
}

Each time the user clicks Run on the query, the dashboard reflects the latest workflow state with the appropriate actions.


Tag Reference

{% signal %}

Sends a signal to a running workflow.

AttributeRequiredDescription
signalNameYesSignal type the workflow listens for
labelYesButton text
domainYesCadence domain
clusterYesCluster name configured in Cadence Web
workflowIdYesTarget workflow execution ID
runIdYesTarget workflow run ID
inputNoSignal payload: true, false, "string", or {"json":"object"}

{% start %}

Starts a new workflow execution.

AttributeRequiredDescription
workflowTypeYesRegistered workflow type name
labelYesButton text
domainYesCadence domain
clusterYesCluster name configured in Cadence Web
taskListYesWorker task list
wfIdNoCustom workflow ID
inputNoWorkflow input
timeoutSecondsNoExecution timeout in seconds
sdkLanguageNo"GO" or "JAVA"

{% image %}

Renders an image with optional size control. Standard markdown images (![alt](url)) also work.

AttributeRequiredDescription
srcYesImage URL
altYesAlt text
widthNoWidth in pixels
heightNoHeight in pixels

Sample Code

Full working examples in both Go and Java:

SampleDescriptionGoJava
MarkdownQueryWorkflowSignals, start buttons, imagesGo sourceJava source
LunchVoteWorkflowInteractive voting with live resultsGo sourceJava source
OrderFulfillmentWorkflowFull ops dashboard with state machineGo sourceJava source
note

Requires Cadence Web v4.0.14+ for MarkDoc rendering support.