Temporal Workflow Tutorial

Temporal Workflow Tutorial for Beginners: Build Your First Durable Workflow

 This Temporal workflow tutorial for beginners walks through setup, your first workflow, activities, workers, and testing, with a full working code example.

If you already know what Temporal is and why teams use it for durable execution, the next question is usually: how do I actually build something with it? That’s exactly what this Temporal workflow tutorial for beginners is going to cover. By the end, you’ll have a working local Temporal setup, a real workflow running end to end, and a solid enough mental model to start building your own.

We’re going to keep this practical. Rather than dumping every concept on you at once, we’ll build a single example, a simple order-processing workflow, piece by piece, and explain each part as we go.

What You’ll Need Before Starting

Like any good Temporal workflow tutorial for beginners, this one assumes basic familiarity with backend development and comfort running commands in a terminal. You don’t need prior Temporal experience.

You’ll need:

  • A computer with Docker installed, or the ability to run the Temporal CLI directly
  • Python 3.9 or later (we’re using Python for this tutorial, since it reads clearly for beginners, though the same concepts apply in Go, Java, TypeScript, and .NET)
  • A code editor of your choice
  • About 30–45 minutes of uninterrupted time

If you haven’t already read our companion piece explaining the core concepts, take a look at our What is Temporal guide first. This tutorial builds directly on the workflow, activity, and worker concepts introduced there.

Step 1: Setting Up Your Local Temporal Environment

The first practical step in any Temporal workflow tutorial is getting a server running locally, since nothing else here works without it.

Before writing any code, you need a running Temporal server to talk to. The fastest way to get one locally is the Temporal CLI, which spins up a full development server with almost no configuration.

Install it based on your operating system. On macOS, using Homebrew:

bash
brew install temporal

On Linux or Windows, you can download the appropriate binary directly from Temporal’s official releases page, or use their install script.

Once installed, start the local dev server:

bash
temporal server start-dev

This single command starts a Temporal server, a built-in database, and the Web UI, all in one process. You should see log output confirming the server is running, and you can open the Web UI at http://localhost:8233 to confirm everything is up. This dashboard is where you’ll watch your workflows execute later in the tutorial, so keep it open in a browser tab.

Step 2: Installing the Python SDK

With the server running, open a new terminal window and set up a project folder for your code:

bash
mkdir temporal-tutorial
cd temporal-tutorial
python3 -m venv venv
source venv/bin/activate
pip install temporalio

The temporalio package is the official Python SDK, and it includes everything you need to define workflows, activities, and workers.

Step 3: Understanding the Pieces You’re About to Build

Before diving into code, here’s the shape of what we’re building. We’ll create a simplified order-processing workflow with three steps:

  1. Charge the customer’s payment
  2. Reserve inventory for the order
  3. Send a confirmation notification

Each of these steps will be an activity, since each one touches something outside our own code, a payment provider, an inventory system, a notification service. The overall sequence, deciding what happens and in what order, will be the workflow itself.

We’ll also need a worker, which is the process that actually runs our workflow and activity code, and a small client script to kick the whole thing off.

Step 4: Writing Your First Activities

Activities are the easiest place to start, since they’re just regular functions with a decorator on top. Create a file called activities.py:

python
from temporalio import activity

@activity.defn
async def charge_payment(order_id: str, amount: float) -> str:
    print(f"Charging {amount} for order {order_id}")
    return f"payment-confirmed-{order_id}"

@activity.defn
async def reserve_inventory(order_id: str) -> str:
    print(f"Reserving inventory for order {order_id}")
    return f"inventory-reserved-{order_id}"

@activity.defn
async def send_confirmation(order_id: str) -> str:
    print(f"Sending confirmation for order {order_id}")
    return f"confirmation-sent-{order_id}"

Each function is decorated with @activity.defn, which registers it as an activity Temporal can call from within a workflow. In a real system, these functions would call actual payment APIs, actual inventory databases, and actual email or SMS services. For this tutorial, we’re just printing output so you can see the sequence execute.

Step 5: Writing Your First Workflow

Now for the part that ties everything together. Create a file called workflows.py:

python
from datetime import timedelta
from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    from activities import charge_payment, reserve_inventory, send_confirmation

@workflow.defn
class OrderWorkflow:
    @workflow.run
    async def run(self, order_id: str, amount: float) -> str:
        await workflow.execute_activity(
            charge_payment,
            args=[order_id, amount],
            start_to_close_timeout=timedelta(seconds=10),
        )

        await workflow.execute_activity(
            reserve_inventory,
            args=[order_id],
            start_to_close_timeout=timedelta(seconds=10),
        )

        await workflow.execute_activity(
            send_confirmation,
            args=[order_id],
            start_to_close_timeout=timedelta(seconds=10),
        )

        return f"Order {order_id} completed successfully"

A few things worth pausing on here, since they trip up almost every beginner:

The start_to_close_timeout is required. Temporal forces you to set a timeout for every activity, since without one, a stuck activity could block a workflow forever. Ten seconds is fine for this tutorial; production timeouts depend on what the activity actually does.

Workflow code must stay deterministic. Notice that our workflow function doesn’t call any external services directly, generate random values, or read the system clock. All of that logic lives inside the activities. This is the single most important rule to internalize early: workflows orchestrate, activities do the actual work.

The import wrapper matters. The workflow.unsafe.imports_passed_through() block tells Temporal’s sandboxing mechanism that these particular imports are safe to pass through without the usual determinism checks, since they’re just plain functions with no side effects at import time.

Step 6: Creating the Worker

The worker is the process that actually executes your workflow and activity code. Create a file called worker.py:

python
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker

from workflows import OrderWorkflow
from activities import charge_payment, reserve_inventory, send_confirmation

async def main():
    client = await Client.connect("localhost:7233")

    worker = Worker(
        client,
        task_queue="order-task-queue",
        workflows=[OrderWorkflow],
        activities=[charge_payment, reserve_inventory, send_confirmation],
    )

    print("Worker started, listening on order-task-queue...")
    await worker.run()

if __name__ == "__main__":
    asyncio.run(main())

This script connects to your local Temporal server, registers the workflow and activities you’ve written, and starts polling the order-task-queue task queue for work. Leave this running in its own terminal window.

Run it:

bash
python worker.py

You should see the “Worker started” message, confirming it’s connected and waiting for tasks.

Step 7: Starting a Workflow from a Client

With the worker running and listening, you need something to actually trigger a workflow execution. Create a file called start_workflow.py:

python
import asyncio
from temporalio.client import Client
from workflows import OrderWorkflow

async def main():
    client = await Client.connect("localhost:7233")

    result = await client.execute_workflow(
        OrderWorkflow.run,
        args=["order-1001", 49.99],
        id="order-workflow-1001",
        task_queue="order-task-queue",
    )

    print(f"Workflow result: {result}")

if __name__ == "__main__":
    asyncio.run(main())

Open a third terminal window (keep the server and worker running in the other two), activate your virtual environment again, and run:

bash
python start_workflow.py

If everything is wired up correctly, you’ll see the client print the final result, and your worker’s terminal will show the three print statements from each activity, in order, as they execute.

Step 8: Watching It Run in the Web UI

This is the part that usually makes Temporal click for beginners. Open http://localhost:8233 in your browser and look for the workflow you just ran, order-workflow-1001. Click into it, and you’ll see the complete event history: every activity that started, every result that was returned, and exactly how long each step took.

This Web UI is one of Temporal’s most practical developer tools. Instead of digging through scattered logs across three files, you get one clean timeline showing exactly what happened to a specific execution, which becomes invaluable once you’re debugging real production issues later on.

Step 9: Simulating a Failure

Here’s where the “durable” part of durable execution actually becomes visible. Stop your worker process (Ctrl+C in that terminal) partway through, then trigger a new workflow, changing the order ID:

bash
python start_workflow.py

Since no worker is listening, the workflow will simply wait, its task sits in the queue with nothing available to pick it up. Now restart your worker:

bash
python worker.py

You’ll see the pending workflow immediately picked up and completed, exactly as if nothing had happened. This is the core guarantee Temporal provides: your workflow’s progress isn’t tied to any single running process. It survives crashes, deployments, and restarts, because the server, not your worker, is the source of truth for what’s happened so far.

Step 10: Adding Retries

Real activities fail sometimes, an API times out, a network blips. Temporal’s retry behavior is configured through a retry policy. Update one of your activity calls in workflows.py:

python
from temporalio.common import RetryPolicy

await workflow.execute_activity(
    charge_payment,
    args=[order_id, amount],
    start_to_close_timeout=timedelta(seconds=10),
    retry_policy=RetryPolicy(
        maximum_attempts=5,
        initial_interval=timedelta(seconds=1),
        backoff_coefficient=2.0,
    ),
)

With this in place, if charge_payment throws an exception, Temporal automatically retries it, waiting one second before the first retry, then doubling the wait each time, up to five total attempts. You didn’t write a single line of retry logic yourself. This is one of the clearest examples of what makes Temporal genuinely useful day to day.

Step 11: Adding a Signal (Optional, but Worth Trying)

Signals let you send information into a running workflow from the outside, useful for things like waiting on a customer’s approval. Here’s a small extension showing the pattern:

python
@workflow.defn
class OrderWorkflow:
    def __init__(self) -> None:
        self._approved = False

    @workflow.signal
    def approve_order(self):
        self._approved = True

    @workflow.run
    async def run(self, order_id: str, amount: float) -> str:
        await workflow.wait_condition(lambda: self._approved)
        # continue with the rest of the workflow steps here

A workflow using this pattern will pause at wait_condition until something sends the approve_order signal, which could happen seconds or weeks later. This is the same mechanism behind human-in-the-loop approval flows, and it’s worth experimenting with once you’re comfortable with the basics above.

Common Beginner Mistakes

A few mistakes come up constantly for people new to Temporal, worth flagging directly so you can avoid them.

Putting business logic that calls external services directly inside a workflow. If a function makes a network call, reads a file, or touches a database, it belongs in an activity, not the workflow function itself.

Forgetting to set timeouts. Every activity call needs a start_to_close_timeout (or similar timeout parameter). Skipping this is a common source of confusing errors early on.

Running the client script before the worker is started. The workflow will simply sit queued and appear to do nothing. Always confirm your worker is running and listening on the correct task queue first.

Mismatched task queue names. The task queue string in your worker and in your client’s execute_workflow call must match exactly, or the worker will never see the work.

Using non-deterministic operations inside a workflow, like datetime.now(), random.random(), or reading environment variables directly. Temporal provides deterministic-safe alternatives for time and randomness inside workflow code specifically for this reason.

Best Practices Once You’re Past the Basics

As you move beyond a first tutorial project, a few habits will save you real pain later:

  • Keep activities small and focused. A single activity that does five unrelated things is harder to retry safely than five separate activities that each do one thing.
  • Version your workflows carefully. Changing a running workflow’s code can break in-flight executions if you’re not careful. Temporal’s documentation covers versioning strategies in depth once you’re ready for that topic.
  • Write unit tests using Temporal’s testing framework, which lets you skip simulated time instantly rather than waiting through real timers during test runs.
  • Use the Web UI constantly during development. It’s the fastest way to understand what actually happened versus what you assumed happened.
  • Read the official SDK documentation for your specific language, since small API details (argument passing, decorators, async patterns) vary slightly between Go, Java, Python, TypeScript, and .NET.

Frequently Asked Questions

Do I need Docker to follow this tutorial? No. The Temporal CLI’s start-dev command runs a full local server without Docker. Docker is an alternative setup method some teams prefer for consistency with production environments, but it isn’t required for learning.

Can I use a language other than Python for this tutorial? Yes. The same workflow, activity, and worker concepts apply across all officially supported SDKs. The syntax differs, but the underlying model, and everything you just built, translates directly.

Why did my workflow seem to hang with no errors? This is almost always a missing or mismatched task queue, or a worker that isn’t running. Double-check that your worker’s task_queue argument matches exactly what your client script is using.

Is this the same code I’d use in production? The structure is close, but production workflows typically add proper error handling, real API integrations inside activities, more thoughtful retry policies, and workflow versioning strategies. This tutorial is meant to build your foundational understanding, not serve as a production template.

What’s the next thing I should learn after this? Once you’re comfortable with this basic workflow, look into child workflows, queries, and workflow versioning. Our What is Temporal article covers the conceptual side of these topics if you want the theory before diving into more code.

Wrapping Up

That’s the full arc of this Temporal workflow tutorial for beginners. You now have a working Temporal setup, a real workflow with three chained activities, a worker executing it, and a client triggering it, all running locally on your own machine. More importantly, you’ve seen firsthand what “durable execution” actually means: stopping and restarting your worker mid-execution didn’t lose any progress at all.

That single demonstration is the entire reason teams adopt Temporal in the first place. Everything else, retries, signals, versioning, scaling workers across a cluster, builds on this same foundation. Spend some time experimenting with this example: break things on purpose, add a fourth activity, try adding an intentional failure to charge_payment and watch the retry policy handle it automatically. That hands-on experimentation is what actually cements how Temporal works, far more than reading about it ever will.

For the official reference documentation as you keep building, the Temporal Python SDK documentation is the best next stop, covering everything from testing to advanced workflow patterns in far more depth than a single tutorial can.

Popular Courses

Leave a Comment