This tutorial walks through a complete local development path for this project:
- Start the Django application and its dependencies with Docker Compose.
- Install Git pre-commit hooks so quality checks run before each commit.
- Create a product through the workflow API.
- Follow the product as Temporal calculates a price, waits for approval, updates the price, and publishes the product.
The intended reader is comfortable opening a terminal but new to Docker Compose and Temporal. You do not need to install Python or PostgreSQL directly on your computer: the local Compose setup runs those application dependencies in containers.
What We Are Building
The project is a Django application backed by PostgreSQL. Product onboarding is asynchronous, so the HTTP request does not perform every step itself. Instead, Django creates the product and starts a Temporal workflow.
The local stack contains:
| Service | Purpose | Local address |
|---|---|---|
django |
Django development server and migrations | http://localhost:8000 |
postgres |
Application database | Internal Compose network |
temporal |
Temporal server | localhost:7233 |
temporal-worker |
Executes workflow code and activities | Internal Compose network |
temporal-ui |
Inspect workflow executions | http://localhost:8088 |
mailpit |
Captures development email | http://localhost:8025 |
The service definitions live in docker-compose.local.yml. Compose gives services stable names on its internal network, which is why Django connects to postgres and the worker connects to temporal instead of localhost.

Prerequisites
Install these tools first:
- Docker Desktop, including Docker Compose
- Git
- just, a command runner used by this repository
Check that they are available:
docker --version
docker compose version
git --version
just --version
Clone the repository and enter it:
git clone https://github.com/klhenams/verbose-spork.git
cd app
Step 1: Check Local Configuration
The Compose file loads environment values from:
.envs/.local/.django
.envs/.local/.postgres
The important local settings are:
-
POSTGRES_HOST=postgres, because PostgreSQL is another Compose service. -
TEMPORAL_HOST=temporal:7233, supplied to the Django and worker containers. -
HUGGINGFACEHUB_API_TOKEN, which is optional for this tutorial.
Do not commit real credentials or API tokens. If your checkout does not contain local environment files, create them from your development configuration. The Django file can contain an empty Hugging Face token while learning the workflow; the application has a deterministic cost-based fallback.
Step 2: Start Docker Compose
The repository's justfile selects docker-compose.local.yml automatically:
just up
Under the hood, this runs:
docker compose -f docker-compose.local.yml up -d --remove-orphans
The first start may take a while because Docker builds the Django and PostgreSQL images and downloads Temporal images. The Django container runs python manage.py migrate before starting the development server, so the database schema is prepared automatically.
Check the running containers:
docker compose ps
Open these pages in a browser:
- Django: http://localhost:8000
- API documentation: http://localhost:8000/api/docs/
- Temporal UI: http://localhost:8088
- Mailpit: http://localhost:8025
If the application is still starting, inspect the logs:
just logs django
just logs temporal-worker
The worker should eventually log that it is listening on the product-tasks task queue. A Temporal worker must be running for workflow activities to execute; starting only Django is not enough.
Step 3: Install Git Pre-Commit Hooks
pre-commit runs configured checks against staged files before Git creates a commit. This repository declares its hooks in .pre-commit-config.yaml and installs the development dependency from pyproject.toml.
Install the hook inside the Django container so the host machine does not need the project's Python version:
docker compose run --rm django pre-commit install
Run all hooks once against the whole repository:
docker compose run --rm django pre-commit run --all-files
The configuration checks common file problems, applies Django upgrades, runs Ruff linting and formatting, formats pyproject.toml, and validates Django templates with djLint. Some hooks can modify files. Review those changes, stage them, and run the command again until it passes.
To run the checks manually on only staged files:
docker compose run --rm django pre-commit run
The configuration deliberately excludes documentation and migrations from the hook selection. That does not mean documentation cannot be reviewed; it means the project avoids applying code-formatting hooks to those paths.
Step 4: Run the Tests
The justfile provides a containerized pytest command:
just pytest
For the pricing activity tests only:
just pytest app/products/tests/test_workflow_activities.py
The pricing activity has two paths:
- With
HUGGINGFACEHUB_API_TOKEN, it asks the configured Hugging Face model for a structuredPriceSuggestion. - Without a token, it uses a fallback of
cost * 1.35, rounded to two decimal places.
That fallback makes the local workflow usable without an external model account.
Step 5: Understand the Product Creation Endpoint
The workflow-specific endpoint is registered as workflows in the product API router. Because the project includes the API router below /api/, create a product with:
POST http://localhost:8000/api/workflows/
The endpoint requires authentication. Use the API documentation at http://localhost:8000/api/docs/ or obtain a DRF token from the project's authentication endpoint before sending the request.
The request body should include the product fields below. category is an existing category ID:
{
"name": "Portable Monitor",
"description": "A lightweight second screen for a laptop.",
"sku": "MONITOR-PORTABLE-001",
"price": "0.00",
"cost": "100.00",
"stock_quantity": 12,
"low_stock_threshold": 3,
"category": 1
}
The initial price is allowed to be a placeholder. The workflow will calculate a suggestion and later write the selected price. The create view saves the product with pending_pricing and starts a workflow with the ID:
product-onboarding-{product_id}
For example, product 42 runs as product-onboarding-42 on Temporal task queue product-tasks.
Step 6: Follow the Workflow Sequentially
The workflow is implemented in app/products/workflows/workflows.py. Its activities are in app/products/workflows/activities.py.

1. Calculate a suggested price
The worker executes calculate_suggested_market_price.
With no Hugging Face token and a cost of 100.00, the fallback suggestion is:
100.00 * 1.35 = 135.00 GHS
The activity returns a structured result containing the suggested price, currency, confidence score, reasoning, and source. If the external model fails, the activity logs the exception and uses the fallback instead.
2. Inspect the suggestion
The API can query the running workflow without changing its state:
GET http://localhost:8000/api/workflows/{product_id}/pricing-suggestion/
Example response:
{
"suggestion": {
"suggested_price": 135.0,
"recommended_margin": "35.0",
"currency": "GHS",
"confidence_score": 0.0,
"reasoning": "Fallback pricing based on product cost.",
"source": "cost_fallback"
}
}
The workflow includes a short ten-second pause after pricing so the execution is easy to observe in the Temporal UI. That delay is for demonstration and is not required by the business process.
3. Approve or override the price
The workflow waits for a signal named approve_suggested_price. Send the suggested price as-is or choose another value:
POST http://localhost:8000/api/workflows/{product_id}/approve-price/
Request body:
{
"price": "129.99"
}
The API translates this request into a Temporal signal. The workflow records the selected price and continues. This is a useful distinction:
- A query reads workflow state.
- A signal sends an event that changes workflow state.
If no approval arrives before the configured timeout, the workflow automatically selects the suggested price. The default timeout is 24 hours.
4. Update the product price
After approval or timeout, update_product_price stores the final price in PostgreSQL. The product remains controlled by the workflow rather than being published immediately by the HTTP request.
5. Publish with retries
The workflow calls publish_product with a Temporal retry policy:
- Maximum of three attempts
- Two-second initial interval
- Exponential backoff with a coefficient of
2.0
When publishing succeeds, the activity changes the product status to active, and the workflow returns:
{
"status": "PUBLISHED",
"final_price": 129.99,
"was_auto_approved": false
}
Step 7: See the Compensation Path
To exercise the failure path, include the test-only field below when creating a product:
{
"name": "Failure Demo",
"description": "Used to demonstrate workflow compensation.",
"sku": "FAILURE-DEMO-001",
"price": "0.00",
"cost": "10.00",
"stock_quantity": 1,
"low_stock_threshold": 1,
"category": 1,
"test_should_fail_publish": true,
"test_timeout_seconds": 20
}
The publish activity raises an error deliberately. Temporal retries it three times. When all attempts fail, the workflow executes revert_product_status, which changes the database status to failed_publish, and returns a FAILED_PUBLISH result.
This is a small Saga pattern: after earlier work has changed state, a compensation activity restores a meaningful failure state when a later operation cannot complete.
The test_* fields are passed through by the current API view to make demonstrations and tests predictable. Treat them as development/testing controls, not as production business inputs.
Observe Everything in Temporal UI
Open http://localhost:8088 and find the workflow named product-onboarding-{product_id}. You can inspect:
- The workflow status
- The activity attempts and retry history
- The waiting period for human approval
- The final return value
- The failure and compensation sequence
This is often easier to understand than reading logs because Temporal shows the workflow's event history as a durable sequence.
Stopping and Resetting the Environment
Stop the containers but keep the PostgreSQL volume:
just down
Stop the containers and delete local volumes, including the database data:
just prune
Use just prune carefully. The next just up starts with an empty database and will need to run migrations and recreate any sample data.
Troubleshooting
The workflow never advances
Check the worker:
just logs temporal-worker
The worker must connect to temporal:7233 and register the product-tasks queue. If only the Django container is running, the workflow can start but its activities cannot be picked up.
Django cannot connect to PostgreSQL
Inside Compose, use postgres as the database host, not localhost. Check that the database health check has passed:
docker compose ps postgres
The pricing suggestion is not from Hugging Face
That is expected when HUGGINGFACEHUB_API_TOKEN is empty or the model request fails. The cost-based fallback is intentionally part of the local implementation.
Pre-commit changes a file
Review and stage the change, then run the hook again:
git diff
git add -A
docker compose run --rm django pre-commit run
What to Read Next
- Docker Compose documentation
- Docker Compose networking
- pre-commit documentation
- Temporal Python SDK documentation
- Temporal workflow signals and queries
- Django REST framework ViewSets
- Django database migrations
The central idea is simple: Django accepts the request and records intent, while Temporal coordinates the long-running work, human decision, retries, and compensation. Docker Compose makes that entire system reproducible on a developer laptop.
Top comments (0)