Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { RunThis } from '@site/src/components/elements/NexusMicroserviceWalkthrough';

:::note Keep these running

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

- The development server, from [step 4](/develop/java/nexus/development-walkthrough?step=implement#start-the-development-server). 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](/develop/java/nexus/development-walkthrough?step=implement#run-the-worker).
- The caller Worker, from [step 6](/develop/java/nexus/development-walkthrough?step=call#call-the-operations-from-a-caller-workflow).

:::

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](/nexus/standalone-activity), as chosen in [step 3](/develop/java/nexus/development-walkthrough?step=backing).

## 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.

<!--SNIPSTART samples-java-nexus-walkthrough-activities-->

[core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalActivities.java](https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalActivities.java)

```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);
}
```

<!--SNIPEND-->

## Back the Operation with it

Use `TemporalOperationHandler`, but start an Activity on the [Client](/nexus/temporal-operation-handler#the-nexus-aware-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.

<!--SNIPSTART samples-java-nexus-walkthrough-notify-requester-->

[core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java](https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java)

```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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think we can remove this from all the code since it is not needed

.setStartToCloseTimeout(Duration.ofSeconds(10))
.setScheduleToCloseTimeout(Duration.ofMinutes(5))
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
.build()));
}
```

<!--SNIPEND-->

### 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](/nexus/standalone-activity#required-options).

## 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.

<!--SNIPSTART samples-java-nexus-walkthrough-handler-worker-->

[core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/HandlerWorker.java](https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/HandlerWorker.java)

```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();
}
```

<!--SNIPEND-->

## Cancellation needs heartbeating

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good information, but not useful to the guide. The guide should stay a bit tighter to How-to-Guide, move this content to Reference.


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](/activity-execution#cancellation).

## Run it

From the command line, this Operation looks like every other one:

<RunThis>

```bash title="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"}'
```

</RunThis>

```
Results:
Status COMPLETED
Result {"deliveredTo":"tao@example.com"}
```

On the handler side there is no Workflow, only an Activity Execution with no parent:

<RunThis>

```bash title="Run 2 of 2: list the Standalone Activity"
temporal activity list --namespace approval-handler-namespace
```

</RunThis>

```
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](/develop/java/nexus/development-walkthrough?step=implement#start-the-development-server).


:::tip RESOURCES

- [Nexus Standalone Activity](/nexus/standalone-activity) for the full concept and options.
- [Standalone Activity](/standalone-activity) for Activity Executions outside a Workflow.
- [Java: Standalone Activities](/develop/java/activities/standalone-activities) for the SDK API.

:::
Loading
Loading