On this page
Guides / Show a run live in your UI

Show a run live in your UI

Relay the Met's stream from your backend to the browser, word by word, without exposing the key and without losing the result if the stream drops.

Updated View .md
StackNode · Express · SSE · browser
Endpointsruns.stream · runs.retrieve

Waiting ten seconds at a spinner feels broken even when it isn't. Meteor delivers the run as Server-Sent Events, and this recipe carries it all the way to the browser.

The part you can't skip: the key is a server-side secret. The browser never talks to Meteor's API; it talks to your backend, and your backend relays.

Before you start

Your key needs runs:execute.

1. The relay in your backend

import express from 'express';
import Met from '@meteor.ia/sdk';

const met = new Met(process.env.MET_API_KEY!, {
  workspaceId: Number(process.env.MET_WORKSPACE_ID),
});

const app = express();

app.get('/api/ask', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();

  try {
    for await (const ev of met.runs.stream({ input: String(req.query.q) })) {
      res.write(`event: ${ev.type}\ndata: ${JSON.stringify(ev.data)}\n\n`);
    }
  } catch (err) {
    res.write(`event: error\ndata: ${JSON.stringify({ message: 'dropped' })}\n\n`);
  } finally {
    res.end();
  }
});

app.listen(3000);

The SDK hands you parsed { type, data } objects. The stream emits a closed set of events:

eventwhen
run.startedthe run began
run.stepa step of the Met, in curated form
run.outputa fragment of the answer
run.completedfinished cleanly
run.failedfinished with an error

2. The browser

<p id="answer"></p>
<script>
  const es = new EventSource('/api/ask?q=' + encodeURIComponent(question));
  const target = document.getElementById('answer');

  es.addEventListener('run.output', (e) => {
    target.textContent += JSON.parse(e.data);   // arrives in fragments
  });
  es.addEventListener('run.completed', () => es.close());
  es.addEventListener('run.failed', () => { target.textContent = "Couldn't answer."; es.close(); });
</script>

EventSource reconnects on its own when the connection drops, and here that works against you: reconnecting to /api/ask runs the Met again, at its cost. That's why it closes explicitly when it's done.

3. Don't lose the result if the stream drops

A stream cut in half doesn't resume transparently. If what you're showing also has to be stored, don't rely on the stream for that: the run exists in the API with or without one.

const run = await met.runs.retrieve(runId);   // the source of truth, unhurried

The pattern for logic that matters is to use the stream only to paint and confirm against retrieve() after the user is gone. If you never store the result, a user who closes the tab at eight seconds leaves behind a run you paid for and nobody read.

Watching it from the terminal

curl -N https://api.met.meteor.com.co/api/v1/workspaces/7/runs \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"input":"Hello","stream":true}'

-N turns off buffering: without it you see everything at once at the end and it looks like streaming isn't working.

Next