---
title: "Workflow Approve: Completeness-Check Dispatch Pattern"
aliases: [approve-source-pattern, workflow-auto-dispatch, pipeline-approve-transition]
tags: [workflow, pipeline, fastapi, python, state-machine, typescript]
sources:
  - "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---

# Workflow Approve: Completeness-Check Dispatch Pattern

When an approval endpoint sits at the boundary of a multi-step pipeline (e.g., "approve source language" before machine translation begins), the handler should check whether all downstream outputs already exist before dispatching new work. If they do — advance state directly to the final review stage. If they don't — dispatch processing and set state to `TRANSLATING` (or equivalent in-progress state).

## Key Takeaways

- An `approve_source` endpoint that blindly advances state without dispatching translation leaves the pipeline permanently stalled — translation tasks never start
- The completeness check and dispatch belong **in the handler**, not in a separate Celery signal or post-save hook — the condition (all languages translated?) is already available at the handler's scope
- Separate the "single language re-trigger" case into its own endpoint (`retranslate-language`) with different validation: EN must be already approved, target lang must not equal source
- TypeScript literal union removal is compile-time-only: removing `'video_native'` from `TranslationMode` causes silent failures in all hardcoded usages — grep for the string first

## Details

### The Broken Pattern

```python
# ❌ BAD — approve_source just flips status, never dispatches translation
@router.post("/{job_id}/approve_source")
async def approve_source(job_id: str, db: AsyncSession = Depends(get_db)):
    job = await get_job(db, job_id)
    job.status = JobStatus.PENDING_FINAL_REVIEW  # ← skips dispatch entirely
    await db.commit()
    return {"status": job.status}
```

Every role can call this and translation silently never starts.

### The Fixed Pattern

```python
# ✅ GOOD — completeness check decides whether to dispatch or advance
@router.post("/{job_id}/approve_source")
async def approve_source(job_id: str, db: AsyncSession = Depends(get_db)):
    job = await get_job(db, job_id)

    # Check if all target languages already have translations
    missing_languages = [
        lang for lang in job.target_languages
        if not await translation_exists(db, job_id, lang)
    ]

    if missing_languages:
        # Some languages still need processing — dispatch and wait
        job.status = JobStatus.TRANSLATING
        await db.commit()
        dispatch_translation_tasks(job_id, missing_languages)
    else:
        # All translations already present — skip to review
        job.status = JobStatus.PENDING_FINAL_REVIEW
        await db.commit()

    return {"status": job.status}
```

### Separate Endpoint for Single-Language Re-trigger

```python
# Distinct endpoint — different validation rules apply
@router.post("/{job_id}/retranslate-language")
async def retranslate_language(
    job_id: str, language: str, db: AsyncSession = Depends(get_db)
):
    job = await get_job(db, job_id)
    assert job.source_approved, "Source must be approved before re-translating"
    assert language != job.source_language, "Cannot re-translate source language"
    # dispatch single-language task...
```

### TypeScript Literal Union: Safe Removal Checklist

When removing a value from a `const` type or literal union:

```typescript
// Before removing 'video_native' from TranslationMode:
// 1. grep for all usages across the codebase
//    grep -r "video_native" src/

// 2. Replace all occurrences with the new value
//    (e.g., 'video_native' → 'traditional')

// 3. Build — TypeScript will catch remaining usages
//    npm run build
```

> [!warning] Compile-time-only failure
> TypeScript literal union mismatches are caught only at build time, not runtime. A value removed from `TranslationMode` will not throw at runtime — it will just pass wrong string values silently until a build reveals the error.

## Related Concepts

- [[wiki/concepts/celery-queue-worker-specialization]] — task dispatch patterns in Celery
- [[wiki/concepts/celery-redis-queue-flush-on-deterministic-error]] — when to purge vs retry queued tasks

## Sources

- [[daily/2026-05-06.md]] — video-accessibility EN-first pipeline: `approve_source` wasn't triggering translation because dispatch was missing from the handler; fixed by adding completeness check and conditional dispatch inside the endpoint
