Temporal Workflows
How DocuStore uses Temporal for durable workflow orchestration, automatic retries, and observable processing pipelines.
Every long-running operation in DocuStore -- PDF parsing, embedding generation, LLM summarization, compound extraction -- is orchestrated by Temporal. This provides automatic retries, timeout handling, observability, and crash recovery without custom retry logic.
Architecture
The system runs as several independent processes:
| Process | Role | Task Queue |
|---|---|---|
| API server | Handles HTTP requests, starts manually triggered workflows | -- |
| Read worker | Subscribes to EventStoreDB, projects events into MongoDB read models | -- |
| Pipeline worker | Subscribes to EventStoreDB, triggers workflows | -- |
| Temporal worker | Executes CPU/IO workflows and activities | artifact_processing |
| LLM worker | Executes LLM workflows and activities | llm_processing |
| Plugin consumer | Consumes Kafka events, starts plugin workflows and runs their workers (only needed when plugins are enabled) | one queue per plugin |
The pipeline worker detects domain events from the EventStoreDB subscription and starts the appropriate Temporal workflow. The two Temporal workers poll their task queues and execute the actual processing logic.
# Start each process individually for local development
make run # API server
make run-read-models # Read worker (EventStoreDB → MongoDB)
make run-workflow-worker # Pipeline worker (EventStoreDB → Temporal)
make run-temporal-worker # Temporal worker (artifact_processing queue)
make run-llm-worker # LLM worker (llm_processing queue)
make run-plugin-consumer # Plugin consumer (Kafka → plugin workflows)
# Or start all six together
make run-allEvent Pipeline
Domain events trigger workflows in a cascade. Each workflow completes by saving results to the aggregate, which emits a new event, which triggers the next workflow:
Artifact.Created
└─ ParseArtifactWorkflow (Docling parse → pages + page text)
├─ Page.Created (each page)
├─ Page.TextMentionUpdated (each page with text)
└─ Artifact.PagesAdded
Page.Created
└─ ExtractCompoundMentionsWorkflow (OCSR)
└─ Page.CompoundMentionsUpdated
├─ EmbedCompoundSmilesWorkflow (ChemBERTa → compound_embeddings)
└─ ReconcileCompoundLabelsWorkflow
Page.TextMentionUpdated
├─ PageSummarizationWorkflow
│ └─ Page.SummaryCandidateUpdated
│ ├─ PageSummaryEmbeddingWorkflow (→ summary_embeddings)
│ └─ once every page of the artifact has a summary:
│ ├─ ArtifactSummarizationWorkflow
│ │ └─ Artifact.SummaryCandidateUpdated
│ │ └─ ArtifactSummaryEmbeddingWorkflow (→ summary_embeddings)
│ └─ BatchReEmbedArtifactPagesWorkflow (all chunks → page_embeddings)
├─ NERExtractionWorkflow
│ └─ Page.TagMentionsUpdated
│ ├─ ArtifactTagAggregationWorkflow
│ │ └─ Artifact.TagMentionsUpdated (→ Qdrant payload sync)
│ ├─ page tags → Qdrant payload sync
│ └─ ReconcileCompoundLabelsWorkflow
└─ DocumentMetadataExtractionWorkflow (triggered by the first page)
└─ Artifact.TitleMentionUpdated / AuthorMentionsUpdated / PresentationDateUpdated
(authors and date → Qdrant payload sync)Text chunks are embedded once per artifact, after all page summaries exist, so that every chunk's context header includes its page summary. GeneratePageEmbeddingWorkflow embeds a single page and is used for manual re-embedding through the API. ReconcileCompoundLabelsWorkflow runs from both the compound and the NER branch, whichever finishes last, to reconcile OCSR labels against NER compound names.
Idempotency
Workflow IDs are derived deterministically from entity IDs:
| Workflow | Workflow ID Pattern |
|---|---|
| Artifact parsing | artifact-parse-{artifact_id} |
| Compound extraction | compound-extraction-{page_id} |
| SMILES embedding | smiles-embedding-{page_id} |
| Compound label reconciliation | reconcile-compound-labels-{page_id} |
| NER extraction | ner-extraction-{page_id} |
| Artifact tag aggregation | artifact-tag-aggregation-{artifact_id} |
| Document metadata | doc-metadata-{artifact_id} |
| Page summarization | page-summarization-{page_id} |
| Page summary embedding | page-summary-embedding-{page_id} |
| Artifact summarization | artifact-summarization-{artifact_id} |
| Artifact summary embedding | artifact-summary-embedding-{artifact_id} |
| Batch chunk embedding | batch-reembed-{artifact_id} |
| Single-page embedding (manual) | embedding-{page_id} |
Workflows use Temporal's ALLOW_DUPLICATE ID reuse policy, so a finished workflow can be re-run under the same ID. Starting a workflow whose ID is still running is rejected by Temporal; the orchestrator logs the rejection and moves on, so duplicate triggers are a safe no-op.
Port-Based Design
The application layer depends on an abstract WorkflowOrchestrator port:
class WorkflowOrchestrator(Protocol):
async def start_embedding_workflow(self, page_id: UUID) -> None: ...
async def start_compound_extraction_workflow(self, page_id: UUID) -> None: ...
async def start_ner_extraction_workflow(self, page_id: UUID) -> None: ...
async def start_artifact_summarization_workflow(self, artifact_id: UUID) -> None: ...
# ... additional workflow methodsThe TemporalWorkflowOrchestrator in the infrastructure layer implements this port using the Temporal Python SDK. The application layer never imports Temporal directly, so the orchestration backend could be swapped to Celery, AWS Step Functions, or any other system without changing business logic.
Task Queues
DocuStore uses two task queues to control concurrency:
| Queue | Purpose | Max Concurrent Activities |
|---|---|---|
artifact_processing | CPU-bound work (parsing, embedding, OCSR, tag aggregation) | Configurable via TEMPORAL_MAX_CONCURRENT_ACTIVITIES |
llm_processing | LLM inference (page and artifact summarization, NER, document metadata) | Configurable via TEMPORAL_MAX_CONCURRENT_LLM_ACTIVITIES |
Each queue is served by its own worker process. Separating LLM activities onto their own queue prevents slow LLM calls from blocking fast embedding and parsing tasks. For local development with Ollama, TEMPORAL_MAX_CONCURRENT_LLM_ACTIVITIES=2 prevents memory exhaustion.
Retry Configuration
Activities are configured with appropriate timeouts and retry policies:
| Workflow | Activity Timeout | Max Attempts | Backoff |
|---|---|---|---|
| PDF parsing | 30 min | 3 | 5s to 60s |
| Compound extraction (OCSR) | 10 min | 3 | 2s to 60s |
| SMILES embedding | 5 min | 3 | 2s to 60s |
| Compound label reconciliation | 2 min | 3 | 2s to 60s |
| NER extraction | 10 min | 3 | 10s to 120s |
| Artifact tag aggregation | 5 min | 3 | 5s to 60s |
| Document metadata | 5 min | 3 | 5s to 60s |
| Page summarization | 15 min | 3 | 5s to 120s |
| Artifact summarization | 60 min | 3 | 5s to 120s |
| Summary embedding (page and artifact) | 5 min | 3 | 5s to 60s |
| Batch chunk embedding | 30 min | 2 | 5s to 60s |
| Single-page embedding | 5 min | 3 | 1s to 30s |
Activity errors go through the retry policy above; errors that a retry cannot fix (not found, validation, not ready) are marked non-retryable. Embedding activities treat "nothing to embed" (a blank page, a deleted artifact) as a completed run, but fail the workflow on embedding provider or internal errors, so the failure shows in the workflow status endpoints instead of passing as success.
Configuration
| Variable | Default | Description |
|---|---|---|
TEMPORAL_ADDRESS | localhost:7233 | Temporal server address |
TEMPORAL_MAX_CONCURRENT_ACTIVITIES | 10 | Max parallel activities on the main queue |
TEMPORAL_LLM_TASK_QUEUE | llm_processing | Task queue name for LLM activities |
TEMPORAL_MAX_CONCURRENT_LLM_ACTIVITIES | 2 | Max parallel LLM activities (Ollama: 1-2, cloud: 5-10) |
Observability
Every workflow execution is visible in the Temporal UI at http://localhost:8233 (default). You can inspect:
- Workflow execution history (started, activities, completed/failed)
- Activity inputs, outputs, and retry attempts
- Pending activities and their timeouts
- Workflow search by ID pattern (e.g., all
ner-extraction-*workflows)