# Show a run live in your UI

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

```ts
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:

| event | when |
|---|---|
| `run.started` | the run began |
| `run.step` | a step of the Met, in curated form |
| `run.output` | a fragment of the answer |
| `run.completed` | finished cleanly |
| `run.failed` | finished with an error |

## 2. The browser

```html
<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.

```ts
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

```bash
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

- The event schema and what `run.step` exposes: [Streaming (SSE)](streaming.html).
- For workspace events rather than a run's: [Live events](events.html).
