Streaming Publish
Progressively update page content
Surflet supports streaming / progressive page updates: an agent publishes an initial page and then appends content blocks incrementally as analysis proceeds. Clients receive updates in real time via SSE (Server-Sent Events), and the VersionBanner automatically detects new versions and prompts users to refresh.
Workflow
1. Agent calls POST /v1/publish → creates initial page (with a loading indicator)
2. Agent analyzes data, producing results progressively
3. Agent calls PATCH /v1/pages/:id/blocks → appends new content blocks
4. Client receives blocks.appended events in real time via SSE
5. VersionBanner prompts the user to refresh and view new content
PATCH /v1/pages/:id/blocks
Appends new content blocks to a published page. The page status must be active or draft.
Request body:
{
"blocks": [
{
"type": "text",
"data": {
"title": "Phase 2 Analysis",
"content": "## User Behavior Analysis\n\nAfter deeper analysis..."
}
},
{
"type": "chart",
"data": {
"title": "Daily Active Users",
"chart_type": "line",
"data": {
"labels": ["Mon", "Tue", "Wed", "Thu", "Fri"],
"datasets": [{"label": "DAU", "data": [1200, 1500, 1300, 1800, 2100]}]
}
}
}
]
}
Response (200):
{
"pageId": "pg_abc123",
"version": 3,
"totalBlocks": 8,
"appendedBlocks": 2,
"appendedBlockIds": ["b_6_a1b2c3d4", "b_7_e5f6g7h8"]
}
Appended blocks without an
idfield receive an auto-generated unique ID.
Python SDK Example
import surflet
import time
client = surflet.Client(api_key="sk_live_...")
# Step 1: Publish the initial page
page = client.publish(
title="CS Weekly Report — 2026-03-20",
page_type="briefing",
blocks=[
surflet.Callout("Analyzing data, please wait...", style="info"),
],
notify="slack:#cs-team",
)
page_id = page["pageId"]
print(f"Page created: {page['pageUrl']}")
# Step 2: Analyze data (simulating a time-consuming operation)
metrics = analyze_weekly_metrics()
# Step 3: Append analysis results
client.append_blocks(page_id, blocks=[
surflet.KeyValue("Weekly Overview", [
{"key": "Total Tickets", "value": str(metrics["total"])},
{"key": "Resolution Rate", "value": f"{metrics['resolution_rate']:.0%}"},
{"key": "Avg Response Time", "value": f"{metrics['avg_response_min']} min"},
]),
surflet.Chart(
chart_type="line",
data={
"labels": metrics["dates"],
"datasets": [{"label": "Tickets", "data": metrics["daily_counts"]}],
},
title="Daily Ticket Trend",
),
])
# Step 4: Continue appending more analysis
top_issues = analyze_top_issues()
client.append_blocks(page_id, blocks=[
surflet.Table(
columns=[
{"key": "issue", "label": "Issue Type"},
{"key": "count", "label": "Count"},
{"key": "trend", "label": "Trend"},
],
rows=top_issues,
title="Top 10 Issues",
),
surflet.Text("## Recommendations\n\n1. Strengthen product QA\n2. Streamline refund process", title="Recommendations"),
])
TypeScript SDK Example
import { Surflet, callout, keyValue, chart } from '@surflet/sdk';
const client = new Surflet({ apiKey: 'sk_live_...' });
// Create the initial page
const page = await client.publish({
title: 'Weekly CS Report',
pageType: 'briefing',
blocks: [callout('Analyzing data...', 'info')],
});
// Append analysis results
await client.appendBlocks(page.pageId, [
keyValue('Summary', [
{ key: 'Total Tickets', value: '156' },
{ key: 'Resolution Rate', value: '94%' },
]),
chart({
chartType: 'line',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
datasets: [{ label: 'Tickets', data: [28, 35, 22, 41, 30] }],
},
title: 'Daily Tickets',
}),
]);
SSE Real-Time Events
Clients can receive real-time page updates via the SSE endpoint:
GET /v1/pages/:pageId/events
This is a long-lived connection; the server pushes events via text/event-stream.
Connecting
const eventSource = new EventSource(
'https://api.surflet.app/v1/pages/pg_abc123/events'
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Event type:', data.type);
console.log('Data:', data.data);
};
Event Types
| Event | Description | Trigger |
|---|---|---|
connected | Connection established | When SSE connection is opened |
blocks.appended | New blocks appended | After PATCH /v1/pages/:id/blocks |
page.updated | Page updated | After PATCH /v1/pages/:id |
action.executed | Action executed | After user clicks an action button |
comment.added | New comment | After a comment is added |
file.uploaded | File uploaded | After a file upload completes |
file.deleted | File deleted | After a file is deleted |
blocks.appended Event Example
{
"type": "blocks.appended",
"data": {
"version": 3,
"newBlocks": [
{"id": "b_6_a1b2c3d4", "type": "text"},
{"id": "b_7_e5f6g7h8", "type": "chart"}
]
}
}
VersionBanner Auto-Detection
The Surflet renderer has a built-in VersionBanner component. When it detects that a page has a new version, it automatically displays an update notice bar at the top of the page. Users can click to refresh and see the latest content. No frontend configuration is required.
Streaming in MCP Clients
In MCP clients (Claude Desktop, Cursor, etc.), agents can use two tools together to implement streaming publish:
surflet_publish— create the initial pagesurflet_append_blocks— append content blocks
Agent: I'll create the report page first, then update it as I analyze.
[surflet_publish] → pg_abc123
[Phase 1 analysis complete]
[surflet_append_blocks] → append metric cards
[Phase 2 analysis complete]
[surflet_append_blocks] → append charts and tables
Agent: Report complete — 3 phases of analysis results have been updated to the page.