Skip to content

Workflow Engine

TheMinecraftGuyGuru edited this page Jun 8, 2026 · 4 revisions

Workflow Engine

Workflows (tapestries) are MuxCore's killer feature. They're multi-step automation pipelines that coordinate many modules to accomplish a goal — like acquiring a movie from request to notification — without hardcoding any step.


What Is a Tapestry?

In the *arr world, workflows are implicit and hardcoded:

Sonarr: Search → Download → Move → Rename → Notify

You can't change the steps. You can't add steps. If you want subtitles fetched before import, that's a separate program (Bazarr) with its own scheduler.

In MuxCore, workflows are explicit, configurable, and extensible:

You define: Request → Metadata → Search → Download → Verify → Extract → Analyze → Transcode → Subtitles → Import → Notify

Each step is handled by a different module. The workflow engine orchestrates — it doesn't implement any step directly.


A Real Example

Here's a tapestry for importing a movie:

id: movie-request
name: "Movie Request Pipeline"
steps:
  - name: metadata-lookup
    handler: metadata-tmdb
    retry: 3
    timeout: 30

  - name: indexer-search
    handler: indexer-prowlarr
    retry: 2
    timeout: 60

  - name: download
    handler: downloader-qbittorrent
    retry: 1
    timeout: 0       # no timeout — downloads can take hours

  - name: verify
    handler: verifier-builtin
    retry: 3
    timeout: 300

  - name: extract
    handler: extractor-unpackerr
    retry: 2
    timeout: 600

  - name: analyze
    handler: analyzer-builtin
    retry: 1
    timeout: 120

  - name: transcode
    handler: transcoder-ffmpeg
    retry: 1
    timeout: 3600

  - name: subtitles
    handler: content-subtitle
    retry: 2
    timeout: 120

  - name: library-import
    handler: media-movies
    retry: 1
    timeout: 60

  - name: notify
    handler: notifier-discord
    retry: 2
    timeout: 10

How It Works

Starting a Tapestry

engine, _ := fabric.Registry.FindByCapability("workflow")
result, err := engine.Run(ctx, "movie-request", map[string]any{
    "title": "The Terminator",
    "quality": "2160p",
})

The Engine Executes Each Step

Step 1: metadata-tmdb → looks up movie details
    ↓ success
Step 2: indexer-prowlarr → searches for releases
    ↓ success  
Step 3: downloader-qbittorrent → starts download, waits for completion
    ↓ success
Step 4: verifier-builtin → checks file integrity
    ↓ success
Step 5: extractor-unpackerr → unpacks archives
    ↓ success
Step 6: analyzer-builtin → checks codec, resolution, bitrate
    ↓ success
Step 7: transcoder-ffmpeg → creates optimized versions
    ↓ success
Step 8: content-subtitle → fetches subtitles
    ↓ success
Step 9: media-movies → imports into library
    ↓ success
Step 10: notifier-discord → sends completion message

If a Step Fails

Step 4: verifier-builtin → FAILS (corrupt download)
    ↓
Retry 1: wait 5 seconds → FAILS
    ↓
Retry 2: wait 25 seconds → FAILS
    ↓
Retry 3: wait 125 seconds → FAILS
    ↓
Tapestry fails → notification sent → manual review

Key Features

Retries with Exponential Backoff

When a step fails, the engine retries with increasing delays:

Attempt 1: immediate
Attempt 2: 5 second delay
Attempt 3: 25 second delay
Attempt 4: 125 second delay

Each step configures its own retry count.

Idempotency

Running the same request twice doesn't cause double downloads or double imports. The engine generates an idempotency key from the request parameters:

idempotencyKey := hash(mediaType + title + quality + season + episode)

If that key was already processed, the engine returns the cached result instead of re-executing.

Timeouts

Each step has its own timeout. Downloads can be unlimited (timeout: 0). Metadata lookups might have 30 seconds. Transcodes might have an hour.


Built-In Tapestries

MuxCore ships with default workflow definitions. Users can modify these or create their own:

Tapestry Purpose
movie-request Full movie acquisition — search through import
tv-request TV episode acquisition
music-request Music album acquisition
book-request Book acquisition
media-transcode Re-encode existing media
media-migrate Move media between storage providers
media-backup Backup to cold storage
library-scan Scan and import existing media files

Note: Built-in tapestries are templates. In practice, you'll customize them for your setup — different downloaders, different notification services, optional steps.


Advanced Features (Planned)

Compensation — Undo on Failure

If step 7 fails after step 3 succeeded, the engine runs compensation handlers:

Step 3: download → started        [compensation: cancel download]
Step 7: import → started          [compensation: remove partial import]
Step 8: notify → not reached

Parallel Steps

Steps that don't depend on each other run simultaneously:

metadata-lookup ─┬─→ indexer-search-1 (torrent)
                 └─→ indexer-search-2 (usenet)
                          │
                    first one wins

Conditional Steps

Skip steps based on results:

- name: transcode
  handler: transcoder-ffmpeg
  condition: "media.codec != 'h264'"   # only transcode if not already h264

Fallback Chains

Try handlers in order until one succeeds:

- name: metadata-lookup
  handlers: [metadata-tmdb, metadata-tvdb, metadata-imdb]

Power Use Cases

Intelligent Orchestration

  • Move workload to idle GPU — transcoding scheduler picks the least-loaded worker
  • Auto-balance storage — move media to the provider with the most free space
  • Prioritize by popularity — recently watched media stays on fast storage
  • Predictive pre-transcoding — transcode the next episode while you're watching the current one

Cross-Media Awareness

A tapestry can coordinate across media types — something impossible in siloed *arr stacks:

User adds an anime series
  → Checks for manga adaptation
  → Checks for light novel source
  → Offers to track and download all related media

Workflow Engine Interface

type WorkflowEngine interface {
    Define(ctx context.Context, def WorkflowDefinition) error
    Run(ctx context.Context, workflowID string, params map[string]any) (string, error)
    Status(ctx context.Context, runID string) (WorkflowRun, error)
    Cancel(ctx context.Context, runID string) error
}

Next Steps

Clone this wiki locally