Why can't a Worflow make API calls directly?

Especially if the API calls are idempotent in nature (i.e. calling them multiple times with same req body is not going to cause any problems).

Context: Trying to see the best way to adopt Temporal in a microservices environment, and don’t completely understand why every microservice will need to be wrapped in an Activity to be called from a workflow.

Workflow executions are not tied to a specific worker that executes your code. If a worker that is currently responsible for executing your code crashes your execution can continue on another one. When this happens (or when a worker evicts a workflow execution out of its cache), your current workflow history has to be replayed from the beginning in order to get it to the exact point where it should be continued.

Activity results are stored in workflow history, and during this history replay, worker is going to use the results of an already completed activity rather than invoking it again. For the case where you make your api calls directly inside workflow code, even if they are idempotent, this code might get executed multiple times (and you would be making many network calls) during a single execution of your workflow. You cannot explicitly control how many times this network calls would be executed (even if idempotent) if you have it in your workflow code directly.

Take a look at this video to learn more about how history replay works.

Temporal activities are executed at least once by default, meaning they have a default retry policy and can be retried in case of failures, you can also disable retries via config if needed. Activities can also heartbeat which allows you to do things like cancellations as well be able to in case of activity failure to resume your activity code from a last known execution point (last heartbeat).
Activities have timeouts that allow you to fine tune their invocations and do things like fail fast in case of issues and do things like compensations.

Since activities can run on their own task queues (end points on the server) they also allow you to do rate limiting (for example not to flood your 3rd party service). You can this way rate limit specific activities individually by setting WorkerOptions->MaxTaskQueueActivitiesPerSecond if needed.

In case of very short running calls and don’t care about rate limiting, you can also use Local Activities. Take a look at this post that shows differences between Activities and Local Activities.

One additional benefits of using activities for microservice invocations are that you can change/update activity code without breaking workflow determinism. In your case, if you need to make changes in your workflow code at some point (especially if its a long-running workflow) you would need to use versioning.
Also, activity code is just your code, so it does not have the the constraints that workflows have regarding workflow determinism.

I would add that a remote API call is by definition is not idempotent even if it always returns the same data. It can succeed a few times and then can fail due to network and configuration issues.