Add messaging to the Service
The approval blocks waiting for a decision. Messages let callers interact with it while it waits.
This step adds three Operations. What the caller needs back decides which message type each one uses:
| Operation | Message type | Why this type |
|---|---|---|
remindApprover | Signal | Fire-and-forget. The caller needs the nudge to happen, not a response. |
getApprovalStatus | Query | Returns the approval's progress without changing it. |
submitDecision | Update | Changes state and returns confirmation that the decision was recorded. |
If the caller can proceed without hearing back, use a Signal. If the caller only needs to read state, use a Query. If the caller needs to know what the message did, use an Update.
Add the handlers to the Workflow
Add a Signal handler that increments the reminder count, a Query handler that reports progress, and an Update handler that records the decision. Recording APPROVED or DENIED satisfies the condition the Workflow is blocked on, so the Workflow 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 - A Query. Reads in-flight progress without changing it. A Query handler must not block
* and must not change Workflow state; it returns only what is already in memory.
*
* <p>It reports whether a decision is still pending, not what the decision was. The decision is
* the result of requestApproval, delivered to the caller without anyone asking.
*/
@QueryMethod
GetApprovalStatusOutput getApprovalStatus();
/**
* 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
All three are @TemporalOperation methods. As described in The Nexus-aware Client, a Signal and a Query are sync messaging and an Update is an async backing.
Signal
Send the Signal through the Client and return a synchronous result. The Operation completes during the handler call.
A synchronous handler must finish inside the 10-second handler deadline, and the real budget is smaller because the clock starts on the caller's side and the request still routes through matching.
One Signal fits easily. A handler that sends several messages or does slow work may not. Overrunning gives the caller a context deadline exceeded error, which it retries with exponential backoff until the schedule-to-close timeout expires.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java
@TemporalOperation
public TemporalOperationResult<Void> remindApprover(
TemporalOperationStartContext ctx, TemporalNexusClient client, RemindApproverInput input) {
client
.getWorkflowClient()
.newWorkflowStub(ApprovalWorkflow.class, ApprovalWorkflowId.forItem(input.getItemId()))
.remindApprover();
return TemporalOperationResult.sync(null);
}
Query
Send the Query through the Client and return its answer as a synchronous result. Like the Signal, the Operation completes during the handler call. Unlike the Signal, it returns the approval's progress: whether a decision is pending, and how many reminders and notes it has.
A Worker answers the Query, so the Operation fails if no Worker is polling the approval's Task Queue.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java
@TemporalOperation
public TemporalOperationResult<GetApprovalStatusOutput> getApprovalStatus(
TemporalOperationStartContext ctx, TemporalNexusClient client, GetApprovalStatusInput input) {
return TemporalOperationResult.sync(
client
.getWorkflowClient()
.newWorkflowStub(ApprovalWorkflow.class, ApprovalWorkflowId.forItem(input.getItemId()))
.getApprovalStatus());
}
Update
Start the Update on the Client. The Operation completes when the Update completes, and the result arrives through the Nexus completion callback. If the Update is already complete when it returns, such as a retried request or one that failed validation, the result returns synchronously.
An Update-backed Operation targets a Workflow that already exists, so submitDecision fails for a purchase with no running approval. As an async backing, it is limited to one per Operation invocation, though the handler can combine it with sync messaging.
Reject a bad Update before it changes anything
An Update can refuse a request; a Signal cannot, because by the time its handler runs, the Signal is already in history.
The approval's validator, the @UpdateValidatorMethod in the Workflow interface above, rejects a second decision for an approval that is already decided. Without it, the later decision would overwrite the first. A rejected Update never runs the handler or reaches Event History, and the caller sees a failed Operation. A validator 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
@TemporalOperation
public TemporalOperationResult<SubmitDecisionOutput> submitDecision(
TemporalOperationStartContext ctx, TemporalNexusClient client, SubmitDecisionInput input)
throws OperationException {
return 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
Don't use a message to fetch the final decision, and don't call getApprovalStatus in a loop waiting for pending to turn false. The decision is the result of requestApproval, and it reaches the caller without anyone asking.
When the handler started the approval, Nexus attached a completion callback to the Workflow. When the Workflow returns, the handler's Namespace delivers the callback to the caller, which records a NexusOperationCompleted event in the caller Workflow's history, and the caller Workflow resumes with the decision. See the asynchronous Operation lifecycle.
The Signal and the Update change a running approval, and the Query reports its progress. None of them returns its outcome. The Signal and the Update also stop working once the approval completes: the Temporal Service accepts a Signal or an Update only while the Workflow is 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 expires. The Query keeps answering after the approval completes, until the Retention Period expires.
If more than one system needs the outcome, step 8 shows how additional callers attach to a running approval and receive the same decision.
Run the messaging Operations
The handler Worker from step 4 already serves these Operations. Nudge the approval left running at the end of step 5. The Signal returns nothing:
temporal nexus operation execute \
--namespace approval-caller-namespace \
--endpoint approval-endpoint \
--service temporal.samples.approval.v1.ApprovalService \
--operation RemindApprover \
--operation-id remind-1 \
--input '{"itemId":"laptop-42"}'
Results:
Status COMPLETED
Result null
The Query reports that the approval is still pending, with the reminder just sent:
temporal nexus operation execute \
--namespace approval-caller-namespace \
--endpoint approval-endpoint \
--service temporal.samples.approval.v1.ApprovalService \
--operation GetApprovalStatus \
--operation-id status-1 \
--input '{"itemId":"laptop-42"}'
Results:
Status COMPLETED
Result {"pending":true,"remindersSent":1,"notesAttached":0}
The Update returns confirmation, including the Workflow's reminder count:
temporal nexus operation execute \
--namespace approval-caller-namespace \
--endpoint approval-endpoint \
--service temporal.samples.approval.v1.ApprovalService \
--operation SubmitDecision \
--operation-id decide-1 \
--input '{"itemId":"laptop-42","decision":"APPROVED"}'
Results:
Status COMPLETED
Result {"recorded":"APPROVED","remindersSent":1}
The decision unblocks the approval Workflow, which returns and completes the requestApproval
Operation from step 5. Collect its result:
temporal nexus operation result \
--namespace approval-caller-namespace --operation-id approval-1
Results:
Status COMPLETED
Result {"decision":"APPROVED"}
Nothing polled for that decision: the Operation completed because the approval Workflow returned.
- Workflow message passing for Signals, Queries, and Updates.
- Handling messages for handler constraints.
- Temporal Operation Handler for the sync messaging and async backing distinction.
Next step
Step 8: Send messages
Signal-with-Start, and several callers awaiting one approval.