A Python tool to export and import Dify workflows/apps as DSL (Domain-Specific Language) YAML files via the Dify console API. This helps teams back up, version control, and migrate workflows between Dify instances they can access.
Dify has no built-in way to manage a fleet of workflows: you export apps one at a time, there's no cross-workflow view of who owns what, and no place to record keep/delete decisions or environment tags. These are long-standing, still-open community requests — batch export (#7966, #26158), workflow-as-code / CI-CD promotion (#32310, #27685), multi-env tags (#23639), creator-level ownership and edit restrictions (#37154), and backup/cleanup (#20645, #29094).
What makes this project different. Several community CLIs already do bulk export/backup and Git sync well — e.g. dify-dsl-pipe, difync, dify-cli, and DifyAI-mcp. None of them — and neither Dify itself — offer governance: a single dashboard of every workflow with owner, keep/delete decision, environment tags, notes, and info-completeness, kept in an editable shared tracker (Confluence) that writes back automatically, with Slack nudges, one-click prune, and author suggestions. Heavyweight enterprise catalogs (SAP AI Agent Hub, Gravitee AI Catalog, ModelOp) cover governance at the org level but are vendor-agnostic, paid, and not Dify-native.
In short: this is the lightweight, self-hosted governance layer that Dify and the existing export CLIs don't give you. Opportunities on the roadmap where competitors are ahead: multi-instance dev→prod promotion, Git-backed versioning / DSL diff, and open-sourcing the governance angle.
# 1. Install Poetry (one-time setup)
curl -sSL https://install.python-poetry.org | python3 -
export PATH="$HOME/.local/bin:$PATH"
# 2. Setup project
git clone <repository-url>
cd dify-console
poetry install
# 3. Configure credentials
cp .env.example .env
# Edit .env with your Dify credentials
# 4. Download/export all workflows from the configured Dify instance
poetry run python src/export.py
# 5. Upload/import all workflows from ./dsl into the configured Dify instance
poetry run python src/import.pyOutput: All workflows are saved as YAML files in ./dsl/ directory.
- ✅ Bulk Export: Export all workflows from your Dify instance in one command
- ✅ Bulk Import: Import exported workflow YAML files into another Dify instance
- ✅ Fast & Concurrent: Uses async requests for fast downloads
- ✅ Automatic Naming: Files are named after your workflow titles
- ✅ Duplicate Handling: Automatically handles workflows with duplicate names
- ✅ Cookie-based Auth: Supports Dify's cookie-based authentication
- ✅ Safe Secret Defaults: Workflow secrets are not exported unless explicitly enabled
- ✅ Error Handling: Retries failed requests automatically
- Python 3.10+
- Poetry (for dependency management)
# macOS/Linux
curl -sSL https://install.python-poetry.org | python3 -
# Add Poetry to your PATH (add to ~/.zshrc or ~/.bashrc)
export PATH="$HOME/.local/bin:$PATH"# Clone the repository
git clone https://github.com/1devspace/dify-console.git
cd dify-console
# Install dependencies
poetry installCreate a .env file in the project root:
cp .env.example .envEdit .env with your Dify instance details:
DIFY_ORIGIN=https://dify.example.com
EMAIL=your-email@example.com
PASSWORD=your-password
DSL_FOLDER_PATH=./dsl
DIFY_INCLUDE_SECRET=falseImportant Notes:
- Remove any trailing slashes from
DIFY_ORIGIN(e.g., usehttps://dify.example.comnothttps://dify.example.com/) - For self-hosted instances, use your full URL (e.g.,
http://localhost:3000) - For cloud instances, use the full domain (e.g.,
https://api.dify.ai) - Keep
DIFY_INCLUDE_SECRET=falsefor normal team sharing. Set it totrueonly if you intentionally need exported secret values in the YAML files. - The tool uses Dify's
/console/apiemail/password login flow. Instances that require SSO, MFA-only login, or a different auth flow may need code changes.
# Make sure Poetry is in your PATH
export PATH="$HOME/.local/bin:$PATH"
# Run the export script
poetry run python src/export.pyThe script will:
- Authenticate with your Dify instance
- Fetch all your workflows/applications
- Download each workflow's DSL file as YAML
- Save them to
DSL_FOLDER_PATH(./dslby default)
Example Output:
Login successful - using cookie-based authentication
Total apps: 25, Total pages: 1
Same name app count: 1, renamed list: ['openai_benchmark_rag -> 【same】openai_benchmark_rag-9c8c995f']
Starting to download YML files...
✅ Downloaded: ./dsl/Benchmarking-prod.yml
✅ Downloaded: ./dsl/demo_RAG.yml
...
All exported workflows are saved in the ./dsl/ directory as YAML files:
dsl/
├── Benchmarking-prod.yml
├── demo_RAG.yml
├── get_history.yml
└── ...
- Files are named after your workflow titles
- Duplicate names are automatically prefixed with
【same】and an ID - Each file contains the complete DSL definition of the workflow
To import all workflows from DSL_FOLDER_PATH (./dsl by default) into the configured Dify instance:
poetry run python src/import.pyTo import one workflow file:
poetry run python src/import.py ./dsl/my-workflow.yml- Configure
.envfor the source Dify instance. - Run
poetry run python src/export.py. - Review the YAML files from
DSL_FOLDER_PATHand redact/rotate secrets before sharing. - Update
.envfor the target Dify instance. - Run
poetry run python src/import.py.
To delete a single workflow by its app ID (no bulk confirmation needed):
poetry run python src/delete.py <app_id>CONFIRM_DELETE_ALL=DELETE_ALL_WORKFLOWS \
poetry run python src/delete.pysrc/sync_tracker.py keeps a Confluence "workflow tracker" page in sync with the
live Dify instance, then posts a status summary to Slack. It is designed to run
unattended on a schedule (see GitHub Actions below), but you can also run it manually.
What it does on each run:
-
Logs in to Dify and fetches every app with its metadata (author, created/updated dates, tags).
-
Reads the current Confluence tracker page (REST API, storage format).
-
Merges by App ID:
- Existing rows are preserved verbatim so human-curated columns
(
Working ?,Decision,Notes,Author & contributor(s),Tags) are never overwritten. - New workflows not yet on the page are appended with
Informations added ? = FALSEand metadata pulled from Dify. - Workflows removed from Dify keep their row but get a red
Removed from Difystatus (history is preserved).
- Existing rows are preserved verbatim so human-curated columns
(
-
Writes the rebuilt page back (version + 1).
-
Posts Slack messages (grouped by contributor so each person sees their own):
- Weekly status + workflows still pending information input (
Informations added ?is notTRUE). - Missing environment tag - workflows whose
Tagslack at least one ofprod/dev/test.
The two categories are sent as separate messages, and if a category is large its per-contributor sections are automatically chunked across additional messages to stay within Slack's 50-block-per-message limit.
- Weekly status + workflows still pending information input (
Add the following to .env (local runs) and as GitHub Actions repository secrets (scheduled runs):
| Variable | Notes |
|---|---|
CONFLUENCE_BASE_URL |
e.g. https://your-site.atlassian.net/wiki (include /wiki) |
CONFLUENCE_EMAIL |
Atlassian account email that owns the API token |
CONFLUENCE_API_TOKEN |
Create at https://id.atlassian.com/manage-profile/security/api-tokens |
CONFLUENCE_PAGE_ID |
Numeric id of the tracker page (e.g. 123456789) |
SLACK_WEBHOOK_URL |
A Slack incoming webhook URL for the target channel |
For the GitHub Action the Dify credentials are read from secrets named
DIFY_ORIGIN, DIFY_EMAIL, and DIFY_PASSWORD.
# venv runner (installs: httpx python-dotenv beautifulsoup4)
./run.sh sync # update Confluence + post to Slack
./run.sh sync --dry-run # compute and print only; change nothing
./run.sh sync --no-slack # update Confluence but skip Slack
# or with Poetry
poetry run python src/sync_tracker.py --dry-runsrc/prune_deleted.py deletes every workflow whose Decision column on the tracker is
Delete (and that still exists in Dify), moves each deleted workflow's exported YAML
into a trashcan folder, then flags those rows red Removed from Dify on the page and
posts a Slack deletion notice. Deletion is permanent, so it is gated behind an explicit
flag.
The trashcan folder defaults to a sibling of DSL_FOLDER_PATH (e.g.
./dsl → ./dsl-trashcan); override it with DSL_TRASHCAN_PATH. It is
git-ignored like the export folders.
./run.sh prune # list candidates only (safe, no deletion)
./run.sh prune --yes # delete + flag Confluence + notify Slack
./run.sh prune --yes --no-slackThis is intentionally a manual step and is NOT part of the weekly schedule.
In addition to the CLI, the project ships a small web app: a FastAPI backend
(src/api) that wraps the existing modules, and a Next.js frontend (frontend/).
It lets the team trigger the actions (sync / env tags / export / prune) from the
browser and watch them run live, plus a governance dashboard of every workflow
(author, env tags, decision, pending/missing-env status) sourced live from Dify +
Confluence.
- Auth: log in with your existing Dify credentials. Your Dify workspace role is
resolved automatically; destructive actions (prune) require an
owner/adminrole. - Jobs: actions run one-at-a-time on the backend (they share the Confluence page,
so they must not overlap) with their logs streamed to the UI. Prune always offers a
dry-run preview and requires typing
DELETEto confirm.
# one-time: install backend + frontend deps
./venv/bin/pip install -e . || ./venv/bin/pip install fastapi "uvicorn[standard]" itsdangerous
(cd frontend && npm install)
# start both (backend on :8008, frontend on :3000)
./dev.sh
# then open http://localhost:3000Or run the two processes separately:
./run.sh serve # backend only (uvicorn, :8008, --reload)
(cd frontend && npm run dev) # frontend only (:3000)Not yet included (planned later phases): persistent job history, in-app workflow health/analysis, and live Dify execution/runtime monitoring.
The web console is single-tenant: one deployment points at one Dify instance,
one Confluence site, and one tracker page, all configured via .env. Every
Dify/Confluence/Slack call runs as the service account in .env; the per-user
login only controls access to the console and resolves roles. (It is not a
multi-tenant SaaS — each team self-hosts its own instance.)
Minimum config to run the console:
See .env.example for the full annotated list. The connection settings (Dify,
Confluence, Slack webhook) can also be edited at runtime from the in-app
Settings tab (admin only); everything else is .env/env-var only.
The app ships with a docker-compose.yml that builds two images — the FastAPI
backend (Dockerfile.api) and the Next.js frontend (frontend/Dockerfile, built
to a standalone server).
cp .env.example .env # fill in real values; set a strong SESSION_SECRET
docker compose up -d --build
# open http://<server-host>:3000Notes:
- Host networking is used so the backend can resolve/reach Dify over your
private network (e.g. Tailscale/NetBird/WireGuard names resolve via the host). This
requires a Linux host; Docker Desktop on macOS/Windows does not support
network_mode: host(on a Mac, run the app natively instead).- Bridge alternative (only if the server reaches Dify via normal DNS/routing):
remove the two
network_mode: hostlines, addports: ["8008:8008"]toapiandports: ["3000:3000"]toweb, and set thewebbuild arg + envBACKEND_URL=http://api:8008.
- Bridge alternative (only if the server reaches Dify via normal DNS/routing):
remove the two
- Persistence: the
appdatavolume holds the UI settings overlay (/app/data/settings.json) and job outputs (/app/data/dsl,…/readable, and the sibling trashcan). Don't run more than oneapireplica — the job runner is in-memory/single-worker. - HTTPS: for plain-HTTP LAN access the defaults are fine. Behind a TLS
reverse proxy, set
https_only=Trueon the session cookie insrc/api/app.pyand pointFRONTEND_ORIGINSat the HTTPS URL.
Dify DSL exports are built for the Dify editor, not for humans: node IDs are opaque
timestamps, the graph is a flat list of nodes + edges, and variable references look like
{{#1752536193945.topic#}}. The readable command turns each DSL into a Markdown report
that is easy to read and review:
- app header (name, mode, description) and a node-type summary,
- a Mermaid flowchart of the graph (renders on GitHub / Confluence / VS Code), with iterations and loops shown as subgraphs,
- a node-by-node breakdown showing the meaningful config per node type (LLM model + prompts, code, HTTP method/URL/body, if-else conditions, tool + parameters, iteration selectors, ...),
- every opaque node ID rewritten to the node's title, so a reference reads
{{Topic+subprompt.topic}}instead of{{#1752536193945.topic#}}. References that cannot be resolved are left raw on purpose, which surfaces broken references in the original flow.
./run.sh readable # convert every workflow in DSL_FOLDER_PATH
./run.sh readable path/to/flow.yml # convert a single file
./run.sh readable some/dir --out docs # convert a folder into ./docsReports are written to ./readable/ (override with READABLE_FOLDER_PATH or
--out), one .md per workflow plus a README.md index. Because the reports embed the same
prompts/code as the DSL, the output folder is gitignored by default.
--output confluence creates/updates one Confluence page per workflow (plus an index page)
inside a target folder, instead of writing local Markdown. The Markdown is rendered to
Confluence storage format: code and prompts become collapsible code macros.
Confluence has no native Mermaid support, so the flow diagram is rendered to an image via
Kroki and uploaded as a page attachment (shown inline), with the Mermaid
source kept in a collapsible block for editing. Use --diagrams code to skip image rendering
and embed the raw Mermaid source as a code block instead.
./run.sh readable --output confluence # all workflows -> configured folder
./run.sh readable --output confluence --space YOURSPACE --parent-id 123456789
./run.sh readable --output confluence --diagrams code # no image renderingBy default diagrams are rendered by the public kroki.io. For privacy (diagram text never
leaves your network) or speed, run Kroki locally and point KROKI_URL at it. Mermaid needs
Kroki's companion service, so start both:
docker run -d --name kroki-mermaid yuzutech/kroki-mermaid
docker run -d --name kroki -p 8000:8000 --link kroki-mermaid \
-e KROKI_MERMAID_HOST=kroki-mermaid yuzutech/kroki
KROKI_URL=http://localhost:8000 ./run.sh readable --output confluenceDiagram rendering is configurable via KROKI_URL, KROKI_FORMAT (svg default, or png),
and KROKI_TIMEOUT (seconds; a slow render falls back to a Mermaid code block).
- An index page ("Dify Workflows — Index") is created under the docs folder you
configure via
--parent-id/--space(or theCONFLUENCE_DOCS_PARENT_ID/CONFLUENCE_DOCS_SPACEenv vars), and each workflow doc is a sub-page of that index. - The index lists every doc page (not just the ones in the current run), so publishing a single workflow refreshes its page without shrinking the index.
- Each doc page includes an "Open in Dify" link to the live workflow (resolved from the
Dify app list; requires reaching the Dify console, otherwise the link is skipped). A
confluence_links.mdmapping is also written to the output folder. - Runs are idempotent: a page is matched by title and updated in place, so re-running refreshes the docs rather than creating duplicates. Workflows that share an app name are disambiguated by appending the source filename.
- Uses the same
CONFLUENCE_BASE_URL/CONFLUENCE_EMAIL/CONFLUENCE_API_TOKENcredentials as the tracker sync.
.github/workflows/sync-tracker.yml runs the sync weekly (Mondays 08:00 UTC) and
exposes a manual Run workflow button (with dry_run / no_slack toggles). After adding
the secrets above, no further setup is required.
If your Dify host is only reachable on a private network, GitHub-hosted runners will not reach it. In that case run
./run.sh syncfrom a machine on that network viacron(e.g.0 8 * * 1 /path/to/dify-console/run.sh sync) or a self-hosted runner.
If you get command not found: poetry, add Poetry to your PATH:
export PATH="$HOME/.local/bin:$PATH"Or add it permanently to your shell config (~/.zshrc or ~/.bashrc):
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc- Check your credentials: Verify your email and password in
.env - Check the URL: Ensure
DIFY_ORIGINis correct and has no trailing slash - Check API access: For self-hosted instances, ensure API access is enabled
- Verify you have workflows in your Dify instance
- Check that your account has access to the workflows
- Ensure the Dify instance URL is correct
- Verify your Dify instance is accessible
- Check network connectivity
- For self-hosted instances, ensure the server is running
- Authentication: The script logs in to Dify using your credentials and receives session cookies
- Token Extraction: Extracts the JWT access token from cookies for API authentication
- Pagination: Fetches all workflows using paginated API requests
- Concurrent Downloads: Downloads DSL files concurrently (up to 3 at a time) for speed
- File Naming: Sanitizes workflow names for filesystem compatibility
dify-console/
├── src/
│ ├── export.py # Main export script
│ ├── import.py # Import workflows script
│ ├── delete.py # Delete workflows script
│ ├── dify_api.py # Dify API client
│ ├── confluence.py # Confluence REST v2 client + storage helpers
│ ├── slack_notify.py # Slack incoming-webhook status message
│ ├── sync_tracker.py # Weekly Confluence sync + Slack notification
│ └── prune_deleted.py # Delete tracker "Delete"-marked workflows + notify
├── .github/workflows/
│ └── sync-tracker.yml # Scheduled (weekly) + manual tracker sync
├── run.sh # Convenience runner (export|import|delete|sync)
├── dsl/ # Exported workflow files (created after export by default)
├── .env # Your credentials (create from .env.example)
├── .env.example # Example configuration file
├── pyproject.toml # Poetry dependencies
└── README.md # This file
- Never commit
.env: The.envfile contains your credentials and should be in.gitignore - Secrets are excluded by default:
DIFY_INCLUDE_SECRET=falseavoids exporting workflow secret values - Review YAML before sharing: If you ever export with
DIFY_INCLUDE_SECRET=true, rotate or remove secrets before sharing - Generated folders are ignored by default:
dsl/is ignored to avoid accidental commits. Share or version workflow YAML only after reviewing/redacting it. - Keep credentials secure: Share credentials only through secure channels
- Use environment variables: For CI/CD, consider using environment variables instead of
.envfiles
Contributions are welcome — see CONTRIBUTING.md for setup and guidelines, and SECURITY.md for reporting vulnerabilities.
This project began as a fork of sattosan/dify-apps-dsl-exporter (the original bulk export/import CLI) and extends it with the Confluence tracker sync, Slack notifications, readable Markdown/Confluence docs, and the governance web console.
MIT — see LICENSE. Original work © 2025 sattosan; additions © 2026 1devspace.
To share this tool with your team:
- Share the repository: Clone or fork this repository
- Each team member sets up their own
.env:- Each person should create their own
.envfile with their credentials - Never commit
.envfiles to version control
- Each person should create their own
- Quick setup for team members:
git clone <repository-url> cd dify-console poetry install cp .env.example .env # Edit .env with their credentials # Download workflows from the configured instance poetry run python src/export.py # Upload workflows from DSL_FOLDER_PATH to the configured instance poetry run python src/import.py
- Export workflows from the source Dify instance.
- Review the YAML files before sharing, especially if secrets were intentionally included.
- Version control reviewed DSL files intentionally if you want history and review; do not commit raw exports blindly.
- Import workflows into the target Dify instance using each team member's own
.env.
For issues or questions:
- Check the troubleshooting section above
- Review the error messages for specific guidance
- Open an issue on the GitHub repository
