# Dify 1.17.1 — Release & Upgrade Intelligence
- **Product:** Dify (https://dify.ai)
- **Version:** 1.17.1
- **Released:** Sep 10, 2026
- **Impact Score:** 7/10
- **Breaking Changes:** Source flagged
- **Canonical Analysis:** https://changelogs.us/dify/updates/1.17.1/

## Executive Summary (TL;DR)
Dify 1.17.1. Source highlight: **Self-hosted deployments using the bundled Weaviate must complete a manual, staged upgrade before starting 1.17.1.** The bundled Weaviate server moves from `1.27.0` to `1.39.2` — 12 minor versions — and skipping minors **is unsupported**. Pulling and restarting can silently and permanently break ve

## Breaking Changes & Deprecations
- - **Transferring workspace ownership left the workspace inconsistent.** The outgoing owner was demoted to admin rather than a restricted member, and the stored owner column was not always updated — so checks that read it, including the guard on the transfer endpoint itself, kept resolving the previous owner. Both now move correctly, and a workspace already left inconsistent by an earlier transfer is repaired on the next one, with no migration required. (#41267)

## New Features
- Knowledge base service-API keys were scoped to the whole workspace: one key could read and write every knowledge base in the tenant, so giving an integration access to a single knowledge base meant giving it access to all of them. Keys can now be bound to specific knowledge bases from the knowledge base's API access panel, with a workspace / this-knowledge-base scope selector at creation time. A bound key returns `403` for any other dataset — both documents and retrieval — and for endpoints that
- The workflow canvas required a mouse to reposition anything. Nodes, multi-node selections, comment markers, and draft comments can now be moved with the arrow keys — hold Shift for larger steps — including Note, Iteration, and Loop nodes. Continuous movement is committed on key release or blur, so it produces one undo entry rather than one per keypress, and read-only mode, container boundaries, and collaboration updates are all still respected. (#41967)
- Plugin authors now have public profile pages at `/marketplace/creator/
- The Markdown form field-name limit was hard-coded at 128 characters, with no way to accommodate longer generated field names. It is now configurable through `MARKDOWN_FORM_FIELD_NAME_MAX_LENGTH` (Docker) or `NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_MAX_LENGTH` (direct Web deployments), defaulting to the previous 128. Character validation and prototype-pollution protections are unchanged. (#41697)
- **`docker-compose.middleware.yaml`**: adds the `local_sandbox` service (present in the main Compose file since 1.16.1) with the `dify_agent_local_sandbox_home` and `dify_agent_local_sandbox_workspace` volumes, and publishes Weaviate's gRPC port `50051`.
- feat(cli): add a compact command map for agent discovery

## Improvements
- **Agent skills bypassed the SSRF private-network policy.** Agent traffic goes through a separate proxy that ignored `SSRF_PROXY_ALLOW_PRIVATE_IPS` and `SSRF_PROXY_ALLOW_PRIVATE_DOMAINS`, so the allowlist you configured for workflows did not constrain the agent runtime. Both proxies now honor the same allowlist. (#41870)
- **A plugin could exhaust worker memory with one declared number.** The blob chunk merger pre-allocated a buffer of the plugin-declared total length before checking it against the file size limit, so a plugin claiming an absurd size crashed the worker with `MemoryError` — the 30 MB limit was never consulted. The declared size is now validated first and rejected with the documented size-limit error. (#42021)
- `h2` bumped from `4.3.0` to `4.4.1` in both `api` and `dify-agent` (#40120, #40119)
- `transformers`, `unstructured`, `pypdf`, `nltk`, `httpx2`, `gitpython`, and `gunicorn` refreshed (#42053, #42054, #42011, #42012, #42017, #41607, #41602, #41505)
- **Agent runs no longer fill up Redis.** Per-run event streams were appended without any cap: production held 3.89 GB across 3,457 streams — 81% of the affected Redis node — with individual streams reaching 30 MB and 78,000 entries, because every stream event including token-scale text deltas was written unbounded and every write refreshed a three-day TTL. Streams are now capped, consecutive text deltas from the same part are coalesced within a 100 ms / 4,096-character window, the cancellation-in
- **The Web Apps sidebar was repainting every loaded app on every navigation.** Moving the list to cursor pagination in 1.17.0 dropped its virtualizer, so every page fetched stayed mounted in the main nav and survived route changes. On a workspace with 427 installed apps, clicking through the top-level nav spent 1,068 ms of 3.06 s compositing across 12,112 paint records — roughly 7.5 fps — with the cost in paint rather than script. The list is virtualized again, now on a single always-virtualized 

## Bug Fixes
- **CSV cells were passed through pandas type inference before indexing.** Leading-zero codes became floats (`00123` → `123.0`), empty cells became the literal string `nan`, and cells containing `NA` or `NULL` became missing values. The extractor now reads everything as text and disables missing-value inference; an explicit `csv_args` still overrides this for callers that want typed parsing. (#41922)
- **Notion table rows lost or gained columns.** Each rich-text segment inside a cell was emitted as its own Markdown column, so any cell with mixed formatting — bold, links, mentions — produced extra columns and corrupted the table, while an empty cell was skipped entirely and shifted the rest of the row left. Cells are now emitted one-per-column with their segments concatenated. (#41993)
- **Notion database properties were truncated at the first formatting change.** `rich_text` and `title` properties read only their first segment, so a title of `Hello world` with `world` in bold was indexed as just `Hello `. All segments are now joined. (#42050)
- **`.xls` cells containing double quotes corrupted the row.** The extractor emits `"column":"value"` pairs; the `.xlsx` path escaped embedded quotes but the legacy `.xls` path interpolated them raw, so `he said "hi"` became `"note":"he said "hi""`. Both engines now behave the same for the same content. (#42049)
- **Any PDF loaded from a URL crashed with a bare `AssertionError`.** After tenant context was added to the PDF extractor, the code path used by `load_from_url` — including the built-in web scraper whenever a target returns `Content-Type: application/pdf` — asserted on an upload record that URL loads never have. Tenant context is now optional, image extraction is skipped when it is absent, and text extraction works again. (#41950)
- **The built-in web scraper returned Python data structures instead of page text.** `readabilipy` hands back `plain_text` as a list of dicts, which was interpolated straight into the output template, so every scraped page came back as `[{'text': '
