Add a Standalone Activity
This step uses the processes started earlier. Leave them running:
The last Operation, notifyRequester, tells the requester whether their approval was APPROVED or DENIED. It is a single outbound notification with no state and nothing to wait for, so it is backed by a Standalone Activity, as chosen in step 3.
Write the Activity
The Activity is ordinary and has nothing Nexus-specific in it; a Workflow could call the same Activity unchanged. In this walkthrough it is a placeholder that sends no email. Real logic would call an email provider, push to a notification service, or write to an outbox.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalActivities.java
public interface ApprovalActivities {
/** STEP 4 placeholder - real logic would apply policy, check limits, or call a risk service. */
@ActivityMethod
void evaluateAutoDecision(String itemId, double amount);
/** STEP 4 placeholder - real logic would page an approver or open a ticket. */
@ActivityMethod
void notifyApproverOfPendingRequest(String itemId, String requester);
/**
* STEP 9 - The Standalone Activity behind the notifyRequester Operation. One outbound
* notification, no state, nothing to wait for. In this sample it only logs; real logic would call
* an email provider, push to a notification service, or write to an outbox.
*/
@ActivityMethod
NotifyRequesterOutput notifyRequester(String requester, NotifyRequesterInput.Decision decision);
}
Back the Operation with it
Use TemporalOperationHandler, but start an Activity on the Client instead of a Workflow. The Operation starts an Activity Execution with no parent Workflow and completes when the Activity returns.
The Activity provides retries, timeouts, and a record of every attempt. The Operation provides the contract, so other teams can trigger the notification without sharing your code or your Namespace.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java
@OperationImpl
public OperationHandler<NotifyRequesterInput, NotifyRequesterOutput> notifyRequester() {
return TemporalOperationHandler.create(
(ctx, client, input) ->
client.startActivity(
ApprovalActivities.class,
ApprovalActivities::notifyRequester,
input.getRequester(),
input.getDecision(),
StartActivityOptions.newBuilder()
// Deriving the Activity Id from the request Id keeps a retried Nexus start
// request targeting the same Activity Execution instead of sending a second
// notification.
.setId("notify-" + ctx.getRequestId())
.setTaskQueue(HandlerWorker.DEFAULT_TASK_QUEUE_NAME)
.setStartToCloseTimeout(Duration.ofSeconds(10))
.setScheduleToCloseTimeout(Duration.ofMinutes(5))
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
.build()));
}
Options an Activity-backed Operation requires
StartActivityOptions needs an Activity Id, unique within the Namespace, because there is no parent Workflow to scope it.
The Task Queue is optional and defaults to the one the Operation runs on. Set it to run notifications on their own Worker fleet.
Derive the Activity Id from the Nexus request Id so that server retries don't send a second email. Retried Nexus start requests carry the same request Id, so every retry lands on the same Activity Id and the notification goes out once. Use the same pattern for any externally visible side effect that can't be undone, such as charging a card, posting to a webhook, or creating a ticket.
To make several Operations share one Activity Execution and all receive its result, derive the Id from the Operation input instead. See Nexus Standalone Activity.
Register the Activity on the Worker
Register the Activity implementation on the Worker that hosts the Nexus Service. An Activity-backed Operation needs no Workflow registration.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/HandlerWorker.java
public static void main(String[] args) {
WorkflowClient client = ClientOptions.getWorkflowClient(args);
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
worker.registerWorkflowImplementationTypes(ApprovalWorkflowImpl.class);
worker.registerActivitiesImplementations(new ApprovalActivitiesImpl());
worker.registerNexusServiceImplementation(new ApprovalServiceImpl());
factory.start();
}
Cancellation needs heartbeating
This notification finishes immediately, so cancellation doesn't matter here. For a longer Activity-backed Operation it does: a cancellation request doesn't interrupt an Activity, so an Activity that never heartbeats runs until it completes or times out. Before shipping a long-running Activity-backed Operation, read Activity cancellation.
Run it
From the command line, this Operation looks like every other one:
temporal nexus operation execute \
--namespace approval-caller-namespace \
--endpoint approval-endpoint \
--service temporal.samples.approval.v1.ApprovalService \
--operation NotifyRequester \
--operation-id notify-1 \
--input '{"requester":"tao@example.com","decision":"APPROVED"}'
Results:
Status COMPLETED
Result {"deliveredTo":"tao@example.com"}
On the handler side there is no Workflow, only an Activity Execution with no parent:
temporal activity list --namespace approval-handler-namespace
Status ActivityId Type StartTime
Completed notify-0f816331-6b85-404d-9dfa-dd249041c1ea NotifyRequester now
The handler derived the Activity Id from the Nexus request Id. Running the command again with a new
--operation-id creates a second Activity Execution, but a server retry of the same request reuses
the first.
If this fails with completion callbacks are not enabled for this namespace, the development
server was started without activity.enableCallbacks=true. See
step 4.
- Nexus Standalone Activity for the full concept and options.
- Standalone Activity for Activity Executions outside a Workflow.
- Java: Standalone Activities for the SDK API.
Next step
Step 10: Finish the flow
The whole approval end to end.