Skip to main content
Unlisted page
This page is unlisted. Search engines will not index it, and only users having a direct link can access it.

Step 7 - Add messaging

View Markdown

The approval blocks waiting for a decision. Messages give callers a way to interact with it while it waits.

Two Operations get added to the workflow sample problem. Which message type each one uses is decided by what the caller needs back, not by preference.

OperationMessage typeWhy this type
remindApproverSignalFire-and-forget. The caller does not need a response, only for the nudge to happen.
submitDecisionUpdateChanges state and returns a result the caller needs — confirmation the decision was recorded.

That is the whole rule. If the caller can proceed without hearing anything back, a Signal is enough. If the caller needs to know what the message did, it needs an Update.

Add the handlers to the Workflow

On the Workflow, add a Signal handler that increments the reminder count and an Update handler that records the decision and unblocks the wait.

The Update is what ends the approval. It records APPROVED or DENIED, which satisfies the condition the Workflow is blocked on, and the Workflow then returns that decision as its result.

core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalWorkflow.java

public interface ApprovalWorkflow {

/**
* The Update handler's name on the wire. The Update-backed Operation has to name the Update
* explicitly when it starts one, so the name is declared once here and reused there rather than
* being spelled as a literal in two places.
*/
String SUBMIT_DECISION_UPDATE = "submitDecision";

/**
* STEP 4 - The Workflow method. Its return value is the result of the requestApproval Operation:
* the Operation completes when this Workflow returns, and the caller receives this value through
* the Nexus completion callback.
*
* <p>Because the Workflow's return value is delivered straight to the caller as the Operation
* result, it has to be the Operation's declared output type.
*/
@WorkflowMethod
RequestApprovalOutput runApproval(RequestApprovalInput input);

/**
* STEP 7 - A Signal. Fire-and-forget: the caller gets no result back, which is why a Signal is
* the right message type for a nudge, and why the contract declares no output for it.
*/
@SignalMethod
void remindApprover();

/**
* STEP 8 - A Signal that also carries supporting information. Reached through Signal-with-Start,
* so it may be the message that creates this Workflow.
*/
@SignalMethod
void attachContext(String note);

/**
* STEP 7 - An Update. The caller needs a result back - confirmation that the decision was
* recorded - which is what makes this an Update rather than a Signal.
*/
@UpdateMethod(name = ApprovalWorkflow.SUBMIT_DECISION_UPDATE)
SubmitDecisionOutput submitDecision(SubmitDecisionInput.Decision decision);

/**
* STEP 7 - The Update's validator. An Update can reject a request before it changes anything,
* which a Signal cannot: a Signal has already been accepted by the time the handler runs.
*
* <p>Here it rejects a second decision for an approval that has already been decided. Without it
* the later decision would silently overwrite the earlier one. A rejected Update does not appear
* in Event History and does not run the handler.
*/
@UpdateValidatorMethod(updateName = ApprovalWorkflow.SUBMIT_DECISION_UPDATE)
void validateSubmitDecision(SubmitDecisionInput.Decision decision);
}

Expose them as Nexus Operations

Both use TemporalOperationHandler, and they divide along the line described in The Nexus-aware Client: a Signal is sync messaging, and an Update is an async backing.

Signal

Send the Signal through the Client, then return a synchronous result. The Operation completes immediately, during the handler call.

The handler has under 10 seconds

A synchronous handler must finish inside the 10-second handler deadline, and the budget you actually get is smaller: the clock starts on the caller's side and the request still has to route through matching.

Sending one Signal is comfortably inside it. A handler that sends several messages, or does slow work before returning, is not. Overrunning gives the caller a context deadline exceeded error, which it then retries with exponential backoff until the schedule-to-close timeout expires.

core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java

@OperationImpl
public OperationHandler<RemindApproverInput, Void> remindApprover() {
return TemporalOperationHandler.create(
(ctx, client, input) -> {
client
.getWorkflowClient()
.newWorkflowStub(
ApprovalWorkflow.class, ApprovalWorkflowId.forItem(input.getItemId()))
.remindApprover();
return TemporalOperationResult.sync(null);
});
}

Update

Start the Update on the Client. This is an async backing: the Operation completes when the Update completes, and its result is delivered through the Nexus completion callback. If the Update happens to come back already complete — a retried request, or one that failed validation — the result returns synchronously instead.

An Update-backed Operation carries two requirements. It targets a Workflow that already exists, so a submitDecision for a purchase with no approval running fails. And because it is an async backing, there is at most one per Operation invocation, though a handler can still combine it with sync side effects.

Reject a bad Update before it changes anything

An Update can also refuse a request, which is the other thing a Signal cannot do. By the time a Signal handler runs the message has already been accepted and written to history; there is nowhere left to say no.

The approval uses that. A validator runs before the handler and rejects a second decision for an approval that has already been decided — without it, the later decision would silently overwrite the earlier one. A rejected Update never runs the handler, never reaches Event History, and surfaces to the caller as a failed Operation.

The validator is the method annotated @UpdateValidatorMethod in the Workflow interface above. It takes the same arguments as the handler, returns nothing, and must not change Workflow state.

core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java

@OperationImpl
public OperationHandler<SubmitDecisionInput, SubmitDecisionOutput> submitDecision() {
return TemporalOperationHandler.create(
(ctx, client, input) ->
client.startWorkflowUpdate(
ApprovalWorkflow.class,
ApprovalWorkflowId.forItem(input.getItemId()),
ApprovalWorkflow::submitDecision,
input.getDecision(),
UpdateOptions.<SubmitDecisionOutput>newBuilder()
.setResultClass(SubmitDecisionOutput.class)
// The Update to invoke has to be named explicitly; the method reference above
// supplies the argument types but not the wire name.
.setUpdateName(ApprovalWorkflow.SUBMIT_DECISION_UPDATE)
// An Update-backed Operation must wait for the ACCEPTED stage. The Operation
// completes later, when the Update completes, through the completion callback.
// Any other stage is rejected with "nexus op workflow updates only support
// WorkflowUpdateStageAccepted for async updates".
.setWaitForStage(WorkflowUpdateStage.ACCEPTED)
.build()));
}

Do not poll for the decision

There is one design mistake worth naming, because it is the most common one in this shape: reaching for a message to fetch the final decision.

The decision is the result of requestApproval, and it reaches the caller without anyone asking for it.

When the handler started the approval, Nexus attached a completion callback to that Workflow. The moment the Workflow returns, the handler's Namespace delivers the callback to the caller's Nexus Machinery, which records a NexusOperationCompleted event in the caller Workflow's history. The caller Worker picks that up on its next Workflow Task, and the caller Workflow resumes with the decision. See the asynchronous Operation lifecycle for the full sequence.

A caller that instead asks the approval for its status in a loop is polling for something already on its way.

Messages are for changing a running approval or nudging it along, not for collecting its outcome. remindApprover asks the approver again. submitDecision supplies the decision and confirms it landed. Neither is a way to read the result.

Both also stop working the moment the approval completes. The Temporal Service accepts a Signal or an Update only while the Workflow is still running, and rejects one sent to a closed Workflow with NOT_FOUND: workflow execution already completed. That happens as soon as the decision lands, not when the Retention Period later expires and the Execution is deleted. A second submitDecision for an approval that has already been decided fails this way, and so does any attempt to use these Operations to look up a past decision.

If more than one system needs the outcome, see step 8 for how additional callers attach to a running approval and receive the same decision.

Next

Step 8 - Send messages - call the messaging Operations, and handle an approval that may not exist yet.

Back to the Microservice Development Walkthrough overview.

RESOURCES