-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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: 10engine, _ := discoveryv1.NewDiscoveryServiceClient(conn).FindByCapability(ctx, &discoveryv1.FindByCapabilityRequest{Capability: "workflow")
result, err := engine.Run(ctx, "movie-request", map[string]any{
"title": "The Terminator",
"quality": "2160p",
})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
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
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.
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.
Each step has its own timeout. Downloads can be unlimited (timeout: 0). Metadata lookups might have 30 seconds. Transcodes might have an hour.
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.
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
Steps that don't depend on each other run simultaneously:
metadata-lookup ─┬─→ indexer-search-1 (torrent)
└─→ indexer-search-2 (usenet)
│
first one wins
Skip steps based on results:
- name: transcode
handler: transcoder-ffmpeg
condition: "media.codec != 'h264'" # only transcode if not already h264Try handlers in order until one succeeds:
- name: metadata-lookup
handlers: [metadata-tmdb, metadata-tvdb, metadata-imdb]- 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
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
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
}- Module System — modules that handle workflow steps
- Event System — how step transitions are communicated
- Writing Modules — build a module that participates in workflows