• Performance and Security
  • Data, AI & Analytics

Getting started with Temporal in Python: Workflow that never fails.

Published On: 18 June 2026.By .
Engineering  —  Backend  —  Python  —  18 June 2026

You have a background job with 5 steps. Your server crashes at step 3. The job restarts from step 1. Temporal fixes that. It saves progress after every step, so if anything crashes it picks up exactly where it stopped. This is a short, practical guide for backend engineers who want reliable, long-running workflows in Python.

Python Temporal Workflow Orchestration Django Durable Execution
Topic
Durable workflows in Python
Tool
Temporal (temporalio SDK)
Language
Python 3.9+
Integrates with
Django
Level
Intermediate backend
Core pieces
Activity, Workflow, Worker, Task Queue
Server port
localhost:7233
Web UI port
localhost:8233
01 - The Problem

When a Crash Means Starting Over

You have a background job with five steps. Your server crashes at step 3. The job restarts from step 1, repeating work that already succeeded and, worse, sometimes repeating side effects that should only happen once.

Temporal fixes this. It saves progress after every step. If anything crashes, the worker restarts and the workflow picks up exactly where it stopped. No re-running completed steps, no lost state, no fragile bookkeeping you have to write yourself.

02 - The Basics

What Is Temporal?

Temporal is a workflow engine. You write workflows in plain Python, and Temporal runs them reliably with retries, timeouts, and durable state, without you having to manage any of that infrastructure yourself.

It has four main pieces. Once these click, everything else follows.

Activity
One function, one job. A DB call, an API call, a file read. This is where all I/O lives.
Workflow
The blueprint that calls activities in order. Deterministic, no direct I/O.
Worker
Your Python process that actually executes the workflow and its activities.
Task Queue
A named channel that connects workflows to the workers that run them.
What is Temporal — diagram showing how Activity, Workflow, Worker, and Task Queue connect in Python
How the four pieces fit together: a Workflow calls Activities, a Worker executes them, connected by a Task Queue
03 - Step One

Writing Activities

An activity is just an async function decorated with @activity.defn. Each one does a single job.

⚠ One rule Import Django models inside the function body, not at the top of the file. Temporal loads your activity file before Django is fully ready, so top-level ORM imports will crash.
activities.py
from temporalio import activity

@activity.defn
async def fetch_order(order_id: int) -> dict:
    from asgiref.sync import sync_to_async
    from orders.models import Order

    def _get():
        order = Order.objects.get(pk=order_id)
        return {"id": order.id, "status": order.status}

    return await sync_to_async(_get)()


@activity.defn
async def process_order(order_id: int) -> str:
    # do the actual processing
    return "processed"
04 - Step Two

Writing a Workflow

A workflow calls activities in sequence. No direct I/O here. No database, no API calls, nothing. Everything goes through activities so Temporal can replay the workflow deterministically.

One more rule Wrap activity imports with workflow.unsafe.imports_passed_through(). Temporal wraps workflow code in a sandbox, and without this your imports get blocked and cause subtle errors.
workflows.py
from temporalio import workflow
from temporalio.common import RetryPolicy
from datetime import timedelta

with workflow.unsafe.imports_passed_through():
    from myapp.activities import fetch_order, process_order


@workflow.defn
class OrderWorkflow:
    def __init__(self):
        self.progress = "Starting"

    @workflow.run
    async def run(self, order_id: int) -> str:
        retry = RetryPolicy(maximum_attempts=3)

        self.progress = "Fetching order"
        order = await workflow.execute_activity(
            fetch_order,
            args=[order_id],
            start_to_close_timeout=timedelta(minutes=2),
            retry_policy=retry,
        )

        self.progress = "Processing order"
        result = await workflow.execute_activity(
            process_order,
            args=[order["id"]],
            start_to_close_timeout=timedelta(minutes=10),
            retry_policy=retry,
        )

        self.progress = "Done"
        return result
05 - Step Three

Running a Worker

The worker connects to Temporal and listens on a task queue.

⚠ Register everything List every workflow and every activity you use. If you forget one, the workflow will hang, waiting for a worker that can handle it.
worker.py
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
from myapp.workflows import OrderWorkflow
from myapp.activities import fetch_order, process_order

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

    worker = Worker(
        client=client,
        task_queue="orders",
        workflows=[OrderWorkflow],
        activities=[fetch_order, process_order],
    )
    await worker.run()

asyncio.run(main())
06 - Step Four

Triggering a Workflow

Start a workflow from your Django app and get back a workflow ID you can use later to query or wait on it.

trigger.py
import asyncio, time
from temporalio.client import Client
from myapp.workflows import OrderWorkflow

async def _start(order_id: int) -> str:
    client = await Client.connect("localhost:7233")
    handle = await client.start_workflow(
        OrderWorkflow.run,
        args=[order_id],
        id=f"order-{order_id}-{int(time.time())}",
        task_queue="orders",
    )
    return handle.id

def trigger_order_workflow(order_id: int) -> str:
    return asyncio.run(_start(order_id))
07 - Step Five

Scheduling a Workflow

Need to run a workflow on a schedule? No cron needed. Temporal has scheduling built in.

schedule.py
from temporalio.client import (
    Client, Schedule, ScheduleActionStartWorkflow,
    ScheduleSpec, ScheduleIntervalSpec
)

async def schedule_daily(order_id: int):
    client = await Client.connect("localhost:7233")

    await client.create_schedule(
        f"daily-order-{order_id}",
        Schedule(
            action=ScheduleActionStartWorkflow(
                OrderWorkflow.run,
                args=[order_id],
                task_queue="orders",
            ),
            spec=ScheduleSpec(
                intervals=[ScheduleIntervalSpec(every=timedelta(days=1))]
            ),
        ),
    )
✓ Why this matters Temporal runs the schedule every day and handles missed runs automatically. No separate cron service to deploy, monitor, or keep in sync.
08 - Observability

Checking Progress with Queries

Want to know what a running workflow is doing without waiting for it to finish? Add a @workflow.query method and call it from anywhere.

workflow query method
# Inside the workflow class
@workflow.query
def get_progress(self) -> str:
    return self.progress
query from Django
# From Django
async def check_status(workflow_id: str) -> str:
    client = await Client.connect("localhost:7233")
    handle = client.get_workflow_handle(workflow_id)
    return await handle.query(OrderWorkflow.get_progress)

We use this to show a live progress bar in the UI while a long job is running.

09 - The Gotcha

workflow.sleep() Is Not asyncio.sleep()

This trips up almost every engineer who starts with Temporal. The two look identical but behave completely differently when a worker restarts.

Wrong
# Wrong - if the worker restarts, this timer is lost
await asyncio.sleep(600)
Correct
# Correct - the timer lives on the Temporal server, survives restarts
await workflow.sleep(timedelta(minutes=10))

We use this to poll a batch job that takes hours:

polling loop
for attempt in range(50):
    is_done = await workflow.execute_activity(check_batch_status, ...)
    if is_done:
        break
    await workflow.sleep(timedelta(minutes=10))

The worker can crash and restart between any two of those polls. Temporal picks up at the right attempt every time.

10 - Tooling

Dev Tip and the Temporal UI

Skip Sleeps Locally

Testing a workflow with 10-minute sleeps is painful. We use a small helper that no-ops the wait in local development.

dev_sleep helper
import os
from temporalio import workflow

async def dev_sleep(duration):
    if os.getenv("DEV_MODE") == "true":
        return  # no wait in local dev
    await workflow.sleep(duration)

Set DEV_MODE=true locally, and the polling loop runs instantly.

The Temporal UI

Temporal comes with a built-in web UI at http://localhost:8233. You can see every workflow that has ever run: its status, inputs, outputs, each activity step, and the full event history. No extra tooling needed.

✓ Why this matters This alone saves hours of debugging. Instead of digging through logs, you open the UI and see exactly where a workflow failed and why.
11 - Cheat Sheet

Quick Summary

Everything in this guide, in one table to keep open while you build.

ConceptWhat to remember
ActivityOne async function. Import Django models inside the body, never at the top.
WorkflowCalls activities, no direct I/O. Use imports_passed_through() for imports.
WorkerRegisters workflows and activities. Must list every activity it uses.
workflow.sleep()Durable. Use it instead of asyncio.sleep() for all delays in a workflow.
@workflow.queryCheck the status of a running workflow without waiting for it to finish.
ScheduleBuilt-in cron replacement. Handles missed runs. No extra tooling.
Temporal UIlocalhost:8233. See every workflow, every step, and every error.
12 - Frequently Asked Questions

Temporal in Python — FAQ

What is Temporal in Python?
Temporal is a workflow orchestration engine. In Python you use the temporalio SDK to write workflows as plain async code, and Temporal runs them reliably with automatic retries, timeouts, and durable state. If your process crashes, Temporal resumes the workflow from exactly where it stopped instead of restarting from the beginning.
What is the difference between a workflow and an activity?
An activity is a single unit of work such as a database call, an API call, or a file read, written as one async function. A workflow is the blueprint that calls activities in order. All I/O and side effects live in activities. Workflow code must be deterministic and should never perform direct I/O.
Why does workflow.sleep() not work like asyncio.sleep()?
asyncio.sleep() runs in memory on the worker process, so if the worker restarts the timer is lost. workflow.sleep() registers a durable timer on the Temporal server, so the wait survives worker restarts and the workflow resumes correctly. Always use workflow.sleep() for delays inside a workflow.
Do I need cron to schedule a Temporal workflow?
No. Temporal has built-in scheduling. You use client.create_schedule with a ScheduleSpec and ScheduleIntervalSpec to run a workflow on an interval. Temporal handles missed runs automatically, so no external cron job is needed.
How do I check the progress of a running workflow?
Add a method decorated with @workflow.query to your workflow class that returns the current state. From your application, get a workflow handle and call handle.query() at any time, even while the workflow is still running. This is commonly used to show a live progress bar in a UI.
Why must Django models be imported inside the activity function?
Temporal loads activity files before Django is fully initialized. A top-level ORM import will crash because the Django app registry is not ready yet. Importing models inside the function body defers the import until the activity actually runs, by which point Django is ready.
What does imports_passed_through() do?
Temporal runs workflow code inside a sandbox to enforce determinism. Wrapping activity imports with workflow.unsafe.imports_passed_through() tells the sandbox to allow those modules through unmodified, which prevents subtle import-related errors in workflow definitions.
What is a Task Queue in Temporal?
A Task Queue is a named channel that connects workflows to workers. When you start a workflow you specify a task queue, and workers listening on that same queue pick up and execute the work. If no worker is listening on the queue, the workflow waits.
How do I run a Temporal worker in Python?
Connect a client to the Temporal server, then create a Worker with the task queue name plus the list of workflows and activities it should handle, and call worker.run(). You must register every workflow and activity the worker uses, or the workflow will hang waiting for a worker that can handle it.
Where is the Temporal Web UI?
The Temporal Web UI runs by default at http://localhost:8233. It shows every workflow that has run, including status, inputs, outputs, each activity step, and the full event history, which makes debugging far faster than reading logs. The Temporal server itself listens on localhost:7233.

Building Reliable Backend Systems?

Durable workflows, data pipelines, and AI systems that hold up at scale are what Auriga IT builds every day. From NHAI processing 20M+ daily transactions to data migrations with zero data loss, this is our home turf.

Related content

Stay Close to What We’re Building

Get insights on product engineering, AI, and real-world technology decisions shaping modern businesses.

Rishabh Jhalani
Rishabh Jhalani
Rishabh Jhalani is a Senior Software Engineer at Auriga IT with over 8 years of experience in Python, Django, GenAI, and agentic AI. He specialises in building scalable, data-driven applications and also writes technical content on Medium.
Go to Top