Skip to main content

Add a Standalone Activity

View Markdown
Keep these running

This step uses the processes started earlier. Leave them running:

  • The development server, from step 4. It must have been started with activity.enableCallbacks=true, or notifyRequester fails with completion callbacks are not enabled for this namespace.
  • The handler Worker, from step 4.
  • The caller Worker, from step 6.

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:

Run 1 of 2: notify the requester
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:

Run 2 of 2: list the Standalone Activity
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.

RESOURCES

Next step​

Step 10: Finish the flow

The whole approval end to end.

→