Surflet

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 id field 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

EventDescriptionTrigger
connectedConnection establishedWhen SSE connection is opened
blocks.appendedNew blocks appendedAfter PATCH /v1/pages/:id/blocks
page.updatedPage updatedAfter PATCH /v1/pages/:id
action.executedAction executedAfter user clicks an action button
comment.addedNew commentAfter a comment is added
file.uploadedFile uploadedAfter a file upload completes
file.deletedFile deletedAfter 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:

  1. surflet_publish — create the initial page
  2. surflet_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.