Bug Report: Pipe emit_message content not persisted to chat storage in v0.8.8
Environment
Open WebUI Version: v0.8.8
Deployment: Docker on Ubuntu 24.04
Database: PostgreSQL
Browser: Chrome 133, Firefox (both tested)
Bug Description
Custom pipe functions using emit_message() execute correctly on the backend but the emitted content is not persisted to the chat message storage. The assistant message in the database has empty content (0 bytes) despite successful emit calls.
Reproduction Steps
Create a pipe function that uses emit_message() to stream content:
SELECTchat->'history'->'messages'->'<message_id>'->>'content'FROMchatWHEREid='<chat_id>';-- Returns empty string despite 277KB of content being emitted
Expected Behavior
Content passed to emit_message() should be:
Streamed to the browser in real-time
Persisted to the assistant message content in the database
Actual Behavior
Content is NOT streamed to browser (blank screen during execution)
Content is NOT persisted to database (assistant message content is empty)
Chat metadata shows activity but no actual content
Evidence from Logs
2026-03-07 23:37:04,375 - Doc Review/2R - INFO - SESSION COMPLETE signal detected
2026-03-07 23:37:04.452 - POST /api/chat/completed HTTP/1.1" 200
The pipe executed 6 rounds of a multi-model conversation, all emits succeeded, but nothing was saved.
Database Evidence
-- Chat has 277KB of JSON but assistant content is empty
SELECTlength(chat::text)FROMchatWHEREid='b7ad9209-2f6d-46bf-be64-7b47e4ac189e';-- Returns: 277916
SELECTlength((chat->'history'->'messages'->'9d24790e-c66d-4ba7-9269-75a73c3765a6'->>'content')::text)FROMchat;-- Returns: 0
Related Issues
#19548 - Custom Pipe Responses Only Appear After Page Reload (November 2025)
#11750 - emit_message error when message doesn't exist
Tried type: "replace" instead of type: "message" - no change
Tried accumulating content before emit - no change
Tried different browsers (Chrome, Firefox, Chrome incognito) - no change
Impact
This bug makes custom pipe functions unusable for any workflow that needs to persist conversation content. The pipe executes correctly but produces no visible or saved output.
Additional Context
This appears to be a regression - similar pipe configurations worked in earlier versions
The emitter function is valid and returns without error
Regular (non-pipe) model streaming works correctly in the same environment
Originally created by @JasmineBlake-Author on GitHub (Mar 8, 2026).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/22381
# Bug Report: Pipe emit_message content not persisted to chat storage in v0.8.8
## Environment
- **Open WebUI Version:** v0.8.8
- **Deployment:** Docker on Ubuntu 24.04
- **Database:** PostgreSQL
- **Browser:** Chrome 133, Firefox (both tested)
## Bug Description
Custom pipe functions using `emit_message()` execute correctly on the backend but the emitted content is not persisted to the chat message storage. The assistant message in the database has empty content (0 bytes) despite successful emit calls.
## Reproduction Steps
1. Create a pipe function that uses `emit_message()` to stream content:
```python
async def emit_message(self, message: str):
await self.__current_event_emitter__(
{"type": "message", "data": {"content": message}}
)
```
2. Execute the pipe via chat
3. Observe:
- Backend logs show `emit_message` being called successfully
- `__current_event_emitter__` is not None (type is `<class 'function'>`)
- Event emitter returns without error
- Chat completes normally (POST /api/chat/completed returns 200)
4. Check database:
```sql
SELECT chat->'history'->'messages'->'<message_id>'->>'content'
FROM chat WHERE id = '<chat_id>';
-- Returns empty string despite 277KB of content being emitted
```
## Expected Behavior
Content passed to `emit_message()` should be:
1. Streamed to the browser in real-time
2. Persisted to the assistant message content in the database
## Actual Behavior
- Content is NOT streamed to browser (blank screen during execution)
- Content is NOT persisted to database (assistant message content is empty)
- Chat metadata shows activity but no actual content
## Evidence from Logs
```
2026-03-07 23:37:04,375 - Doc Review/2R - INFO - SESSION COMPLETE signal detected
2026-03-07 23:37:04.452 - POST /api/chat/completed HTTP/1.1" 200
```
The pipe executed 6 rounds of a multi-model conversation, all emits succeeded, but nothing was saved.
## Database Evidence
```sql
-- Chat has 277KB of JSON but assistant content is empty
SELECT length(chat::text) FROM chat WHERE id = 'b7ad9209-2f6d-46bf-be64-7b47e4ac189e';
-- Returns: 277916
SELECT length((chat->'history'->'messages'->'9d24790e-c66d-4ba7-9269-75a73c3765a6'->>'content')::text) FROM chat;
-- Returns: 0
```
## Related Issues
- #19548 - Custom Pipe Responses Only Appear After Page Reload (November 2025)
- #11750 - emit_message error when message doesn't exist
- #20196 - AsyncGenerator pipes hang in UI
## Workaround Attempted
- Tried `type: "replace"` instead of `type: "message"` - no change
- Tried accumulating content before emit - no change
- Tried different browsers (Chrome, Firefox, Chrome incognito) - no change
## Impact
This bug makes custom pipe functions unusable for any workflow that needs to persist conversation content. The pipe executes correctly but produces no visible or saved output.
## Additional Context
- This appears to be a regression - similar pipe configurations worked in earlier versions
- The emitter function is valid and returns without error
- Regular (non-pipe) model streaming works correctly in the same environment
@pr-validator-bot commented on GitHub (Mar 8, 2026):
⚠️ Missing Issue Title Prefix
@JasmineBlake-Author, your issue title is missing a prefix (e.g., bug:, feat:, docs:).
Please update your issue title to include one of the following prefixes:
bug: Bug report or error you've encountered
feat: Feature request or enhancement suggestion
docs: Documentation issue or improvement request
question: Question about usage or functionality
help: Request for help or support
Example: bug: Login fails when using special characters in password
<!-- gh-comment-id:4017789010 -->
@pr-validator-bot commented on GitHub (Mar 8, 2026):
# ⚠️ Missing Issue Title Prefix
@JasmineBlake-Author, your issue title is missing a prefix (e.g., `bug:`, `feat:`, `docs:`).
Please update your issue title to include one of the following prefixes:
- **bug**: Bug report or error you've encountered
- **feat**: Feature request or enhancement suggestion
- **docs**: Documentation issue or improvement request
- **question**: Question about usage or functionality
- **help**: Request for help or support
Example: `bug: Login fails when using special characters in password`
Thank you for the detailed report and reproduction steps. This is a known architectural limitation with how pipes interact with the event persistence system, not a regression specific to v0.8.8.
Root Cause
The backend event emitter does persist type: "message" events to the database — this part works correctly. However, after your pipe's pipe() method finishes executing, the frontend performs a full chat history save (via chatCompletedHandler), which writes the entire local message state back to the database.
The problem is:
Your pipe emits content via type: "message" events → backend writes it to the DB ✅
Your pipe returns None (no explicit return value) → the HTTP response carries no content
The frontend's local copy of the assistant message still has content: "" (or whatever accumulated via Socket.IO — which depends on timing)
chatCompletedHandler fires → saves the full local history to the DB → overwrites the event-emitter-written content with the empty local state
This explains exactly what you see: 277KB of chat JSON (the overall chat structure is saved), but the assistant message's content field is empty (overwritten by the empty local state).
Workaround
The fix is to return or yield your content from the pipe() method instead of relying solely on emit_message:
# ❌ Fragile — content can be overwritten on saveasyncdefpipe(self,body:dict,__event_emitter__=None):await__event_emitter__({"type":"message","data":{"content":"Hello!"}})# Returns None — frontend overwrites with empty content# ✅ Works reliably — return content directlyasyncdefpipe(self,body:dict,__event_emitter__=None):await__event_emitter__({"type":"status","data":{"description":"Working...","done":False}})result=do_work()await__event_emitter__({"type":"status","data":{"description":"Done","done":True}})returnresult# ✅ Also works — yield for streamingasyncdefpipe(self,body:dict,__event_emitter__=None):await__event_emitter__({"type":"status","data":{"description":"Streaming...","done":False}})forchunkingenerate_chunks():yieldchunkawait__event_emitter__({"type":"status","data":{"description":"Done","done":True}})
Events like status, source, citation, files, embeds, and notification are safe to use alongside return/yield — they update separate fields that aren't affected by the content save.
Documentation Update
We've updated the Events documentation to explicitly document this behavior.
The changes will go live soon.
The key takeaway: for Pipes, always deliver message content via return/yield. Use events for supplementary data (status, sources, files, notifications), not as the sole content delivery mechanism.
<!-- gh-comment-id:4017833434 -->
@Classic298 commented on GitHub (Mar 8, 2026):
Thank you for the detailed report and reproduction steps. This is a known architectural limitation with how pipes interact with the event persistence system, not a regression specific to v0.8.8.
## Root Cause
The backend event emitter **does** persist `type: "message"` events to the database — this part works correctly. However, after your pipe's `pipe()` method finishes executing, the frontend performs a full chat history save (via `chatCompletedHandler`), which writes the entire local message state back to the database.
The problem is:
1. Your pipe emits content via `type: "message"` events → backend writes it to the DB ✅
2. Your pipe returns `None` (no explicit return value) → the HTTP response carries no content
3. The frontend's local copy of the assistant message still has `content: ""` (or whatever accumulated via Socket.IO — which depends on timing)
4. `chatCompletedHandler` fires → saves the full local `history` to the DB → **overwrites** the event-emitter-written content with the empty local state
This explains exactly what you see: 277KB of chat JSON (the overall chat structure is saved), but the assistant message's `content` field is empty (overwritten by the empty local state).
## Workaround
The fix is to **return or yield your content** from the `pipe()` method instead of relying solely on `emit_message`:
```python
# ❌ Fragile — content can be overwritten on save
async def pipe(self, body: dict, __event_emitter__=None):
await __event_emitter__({"type": "message", "data": {"content": "Hello!"}})
# Returns None — frontend overwrites with empty content
# ✅ Works reliably — return content directly
async def pipe(self, body: dict, __event_emitter__=None):
await __event_emitter__({"type": "status", "data": {"description": "Working...", "done": False}})
result = do_work()
await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}})
return result
# ✅ Also works — yield for streaming
async def pipe(self, body: dict, __event_emitter__=None):
await __event_emitter__({"type": "status", "data": {"description": "Streaming...", "done": False}})
for chunk in generate_chunks():
yield chunk
await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}})
```
Events like `status`, `source`, `citation`, `files`, `embeds`, and `notification` are safe to use alongside return/yield — they update separate fields that aren't affected by the content save.
## Documentation Update
We've updated the Events documentation to explicitly document this behavior.
The changes will go live soon.
The key takeaway: **for Pipes, always deliver message content via return/yield. Use events for supplementary data (status, sources, files, notifications), not as the sole content delivery mechanism.**
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Originally created by @JasmineBlake-Author on GitHub (Mar 8, 2026).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/22381
Bug Report: Pipe emit_message content not persisted to chat storage in v0.8.8
Environment
Bug Description
Custom pipe functions using
emit_message()execute correctly on the backend but the emitted content is not persisted to the chat message storage. The assistant message in the database has empty content (0 bytes) despite successful emit calls.Reproduction Steps
emit_message()to stream content:Execute the pipe via chat
Observe:
emit_messagebeing called successfully__current_event_emitter__is not None (type is<class 'function'>)Check database:
Expected Behavior
Content passed to
emit_message()should be:Actual Behavior
Evidence from Logs
The pipe executed 6 rounds of a multi-model conversation, all emits succeeded, but nothing was saved.
Database Evidence
Related Issues
Workaround Attempted
type: "replace"instead oftype: "message"- no changeImpact
This bug makes custom pipe functions unusable for any workflow that needs to persist conversation content. The pipe executes correctly but produces no visible or saved output.
Additional Context
@pr-validator-bot commented on GitHub (Mar 8, 2026):
⚠️ Missing Issue Title Prefix
@JasmineBlake-Author, your issue title is missing a prefix (e.g.,
bug:,feat:,docs:).Please update your issue title to include one of the following prefixes:
Example:
bug: Login fails when using special characters in password@Classic298 commented on GitHub (Mar 8, 2026):
Thank you for the detailed report and reproduction steps. This is a known architectural limitation with how pipes interact with the event persistence system, not a regression specific to v0.8.8.
Root Cause
The backend event emitter does persist
type: "message"events to the database — this part works correctly. However, after your pipe'spipe()method finishes executing, the frontend performs a full chat history save (viachatCompletedHandler), which writes the entire local message state back to the database.The problem is:
type: "message"events → backend writes it to the DB ✅None(no explicit return value) → the HTTP response carries no contentcontent: ""(or whatever accumulated via Socket.IO — which depends on timing)chatCompletedHandlerfires → saves the full localhistoryto the DB → overwrites the event-emitter-written content with the empty local stateThis explains exactly what you see: 277KB of chat JSON (the overall chat structure is saved), but the assistant message's
contentfield is empty (overwritten by the empty local state).Workaround
The fix is to return or yield your content from the
pipe()method instead of relying solely onemit_message:Events like
status,source,citation,files,embeds, andnotificationare safe to use alongside return/yield — they update separate fields that aren't affected by the content save.Documentation Update
We've updated the Events documentation to explicitly document this behavior.
The changes will go live soon.
The key takeaway: for Pipes, always deliver message content via return/yield. Use events for supplementary data (status, sources, files, notifications), not as the sole content delivery mechanism.