I take it that the work of this Activity happens in Service B, and the work takes too long for a synchronous call like this?
async function myActivity() {
  await result = serviceB.doWork()
  return result
}
In these cases, I’d recommend considering options 1 and 2 before the options you described (3 & 4):
1. Service B runs a Worker
- The Workflow in Service A starts activityFooontaskQueue: service-B.
- Service B runs a Worker that listens on taskQueue: service-Band only registers/handlesactivityFoo.
Pro: Simplest
2. Service A starts the Activity, Service B completes it
- The Workflow in Service A starts activityFoo
- The Worker in Service A picks up activityFoo
- The Activity does:
async function activityFoo() {
  const info = Context.current().info
  const idempotencyToken = getIdempotencyToken()
  await serviceB.startWork({
    fullActivityId: {
      activityId: info.activityId,
      workflowId: info.workflowExecution.workflowId,
      runId: info.workflowExecution.runId,
    },
    idempotencyToken,
  })
  throw new CompleteAsyncError()
}
// Note: there are different ways to create idempotency tokens, depending on how you 
// run your Workflow—whether it can be retried, Continued As New, or restarted by a Client.
// This implementation may not be what you want.
function getIdempotencyToken() {
  return Context.current().info.workflowExecution.runId + Context.current().info.activityId
}
- Service B uses AsyncCompletionClientto heartbeat, and when it’s done with the work, does:
await asyncClient.complete(fullActivityId, "Job's done!")
Con: You have to pass the fullActivityId and create an idempotency token.
3. Service A polls Service B
- Service A runs the Activity, which first tells Service B to do the work, and then heartbeats and polls Service B until it’s done. Something like:
async function activityFoo() {
  const info = Context.current().info
  const idempotencyToken = getIdempotencyToken()
  await serviceB.startWork({
    fullActivityId: {
      activityId: info.activityId,
      workflowId: info.workflowExecution.workflowId,
      runId: info.workflowExecution.runId,
    },
    idempotencyToken,
  })
  while (true) {
    Context.current().heartbeat()
    const response = await serviceB.getStatus({ idempotencyToken })
    if (response.completed) {
      return response.result
    }
    await Context.current().sleep('1 min')
  }
}
Con: In addition to #2 con, there’s more code and network requests.
4. Service B reports completion via API call
- Service A runs an Activity that includes serviceB.startWork(), as in scenario #2
- Service A runs an API server with a /complete/{runId}/{activityId}endpoint (or equivalent gRPC method)
- When Service B is done with the work, it calls the /completeendpoint
- The Service B API handler uses the AsyncCompletionClientto complete the Activity
Cons:
- The API server is added complexity.
- No heartbeating, which means Temporal won’t know when Service B stops doing the work—Temporal will have to wait until the scheduleToCloseorstartToClosetimeout to know it failed. And Service B won’t know if the Activity gets Cancelled. So ideally Service B would heartbeat, but if it’s going to do that, it might as well report completion as well, in which case we can do scenario #2.