BRIDGIT / INTERACTION LIBRARY

What your agent can put on your phone

34 ready-made cards from everyday life: approve a payment, pick a table, plan a day out, check the budget, take the evening medicines, sign a lease, quiz the kids. Try each one below, see exactly what the agent gets back, and copy the JSON to send your own.

For agents

The whole library is one JSON file. Each entry links to the exact body for POST /v1/interactions: fetch it, change the words, send it.

curl -s https://bridgit.cc/interactions/index.json
# BASE is the base URL from pairing; it already ends in /v1
curl -s https://bridgit.cc/interactions/approve-payment.json \
  | curl -s -X POST "$BASE/interactions" -H "Authorization: Bearer $KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" --data-binary @-

Questions answer as answer.button + answer.values. HTML cards answer with whatever the page passes to Bridgit.respond(), as answer.data. Text panels take POST /v1/text/stream. Every endpoint and limit is in your phone's GET /v1/guide.

On a real tablet

Four of these cards sent from this library to a Galaxy Tab A11 running Bridgit, through the API, as an agent would.

Sign the lease card on a Galaxy Tab A11
Sign the lease
Pick the listing photo card on a Galaxy Tab A11
Pick the listing photo
Cinema seats card on a Galaxy Tab A11
Cinema seats
Kids quiz card on a Galaxy Tab A11
Kids quiz

Money

Your agent
Pay €184.20 to Fraulini Plumbing?

Invoice #2291 for the boiler repair on 28 Oct. Due 7 Nov. It matches the quote you approved (€180 + €4.20 parts).

I’ll pay from your current account ending 0932.

QuestionMoney

Pay €184.20 to Fraulini Plumbing?

Ask before paying a bill. The simplest useful card: two buttons and the facts.

Owner sees
The invoice details and Pay / Not now buttons.
Then
pay: make the payment and send a notice with the confirmation number. not_now: stop. Ask again only when the owner asked you to or brings it up.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "Pay €184.20 to Fraulini Plumbing?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "pay",
    "values": {}
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.5 KB
{
  "kind": "question",
  "title": "Pay €184.20 to Fraulini Plumbing?",
  "text": "Invoice #2291 for the boiler repair on 28 Oct. Due 7 Nov. It matches the quote you approved (€180 + €4.20 parts).\n\nI’ll pay from your current account ending 0932.",
  "controls": [
    {
      "type": "button",
      "id": "pay",
      "label": "Pay €184.20",
      "style": "primary"
    },
    {
      "type": "button",
      "id": "not_now",
      "label": "Not now"
    }
  ]
}
Raw JSONbuttonsapproval
Your agentUrgent🔊 Read aloud
Card declined: 412.00 zł at MediaExpert

Your Visa ending 4417 was just declined online at 21:14. Fraud check flagged it: new device, different city.

Was this you?

Call the bank ↗Calls +48800123456
QuestionMoney

Card declined: 412.00 zł at MediaExpert

Urgent, read aloud, with a one-tap call to the bank. For things that cannot wait.

Owner sees
An urgent card that is spoken aloud: Retry / Freeze card buttons and a Call the bank link.
Then
retry: retry the payment once. freeze: freeze the card through the bank’s API and confirm with a notice. The Call link only opens the dialer; it is not an answer.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "Card declined: 412.00 zł at MediaExpert",
  "priority": "urgent",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "freeze",
    "values": {}
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.6 KB
{
  "kind": "question",
  "title": "Card declined: 412.00 zł at MediaExpert",
  "text": "Your Visa ending 4417 was just declined online at 21:14. Fraud check flagged it: new device, different city.\n\nWas this you?",
  "priority": "urgent",
  "speak": true,
  "expires_in_seconds": 900,
  "controls": [
    {
      "type": "button",
      "id": "retry",
      "label": "Yes, that was me",
      "style": "primary"
    },
    {
      "type": "button",
      "id": "freeze",
      "label": "No, freeze the card"
    },
    {
      "type": "link",
      "id": "call_bank",
      "label": "Call the bank",
      "url": "tel:+48800123456"
    }
  ]
}
Raw JSONurgentspeaklinktel
HTML cardMoney

Split dinner at Trattoria Sole

A mini app: tip chips and a slider per person, starting from what each one ordered.

Owner sees
The bill total, tip buttons, and a slider and amount for each diner.
Then
Send each person a payment request for their share.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Split dinner at Trattoria Sole",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "total_eur": 233.64,
      "tip_percent": 10,
      "shares_eur": {
        "you": 68.75,
        "maya": 63.8,
        "tom": 54.89,
        "jo": 46.2
      },
      "request_from": [
        "maya",
        "tom",
        "jo"
      ]
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 4.7 KB
{
  "kind": "html",
  "title": "Split dinner at Trattoria Sole",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#fbf7f2;color:#2a211b;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:600px;margin:0 auto;padding:20px 16px 120px}\nh1{font-size:23px;margin:0 0 2px}\n.sub{color:#7a6d62;margin:0 0 14px}\n.total{display:flex;justify-content:space-between;align-items:baseline;background:#2a211b;color:#fff;border-radius:16px;padding:14px 16px;margin-bottom:14px}\n.total b{font-size:28px}\n.total span{color:#cbbfb3;font-size:14px}\n.tip{display:flex;gap:8px;margin-bottom:16px}\n.tip button{flex:1;font:inherit;font-weight:600;border:2px solid #e6dccf;background:#fff;border-radius:12px;padding:10px 4px;min-height:46px;cursor:pointer}\n.tip button.on{border-color:#e2683c;background:#fff1ea;color:#a8401b}\n.p{background:#fff;border-radius:14px;padding:12px 14px;margin-bottom:10px;box-shadow:0 1px 0 #ece3d8}\n.p .h{display:flex;align-items:center;gap:10px}\n.av{width:36px;height:36px;border-radius:50%;display:grid;place-content:center;font-weight:700;color:#fff;flex:none}\n.p .n{flex:1;font-weight:600}\n.p .amt{font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}\n.p .what{font-size:13px;color:#7a6d62;margin:4px 0 6px 46px}\ninput[type=range]{width:calc(100% - 46px);margin-left:46px;accent-color:#e2683c;height:28px}\n.bar{position:fixed;left:0;right:0;bottom:0;background:#fffdfa;border-top:1px solid #ece3d8;padding:12px 16px 12px}\n.bar .in{max-width:600px;margin:0 auto;display:flex;gap:10px;align-items:center}\n.bar p{flex:1;margin:0;font-size:14px;color:#7a6d62}\n.go{font:inherit;font-weight:700;border:0;border-radius:12px;padding:14px 18px;min-height:48px;background:#e2683c;color:#fff;cursor:pointer}\n.ghost{font:inherit;border:0;background:none;color:#7a6d62;padding:10px;cursor:pointer}\n</style>\n<div class=\"wrap\">\n  <h1>Split dinner at Trattoria Sole</h1>\n  <p class=\"sub\">I read the receipt you photographed. Shares start from what each person ordered; slide to adjust.</p>\n  <div class=\"total\"><div><span>Bill</span><br><b id=\"tot\"></b></div><span id=\"sub2\"></span></div>\n  <div class=\"tip\" id=\"tip\"><button data-t=\"0\">No tip</button><button data-t=\"10\" class=\"on\">10%</button><button data-t=\"12.5\">12.5%</button><button data-t=\"15\">15%</button></div>\n  <div id=\"ppl\"></div>\n</div>\n<div class=\"bar\"><div class=\"in\"><button class=\"ghost\" id=\"even\">Split evenly</button><p></p><button class=\"go\" id=\"go\">Send requests</button></div></div>\n<script>\nvar food=212.40,tip=10,sent=false,people=[\n {id:'you',name:'You',c:'#e2683c',what:'Risotto, tiramisu, 2 wine',w:62.5},\n {id:'maya',name:'Maya',c:'#2f9e8f',what:'Burrata, sea bass',w:58.0},\n {id:'tom',name:'Tom',c:'#5b6ee1',what:'Pizza, 3 beers',w:49.9},\n {id:'jo',name:'Jo',c:'#c2477e',what:'Salad, sparkling water',w:42.0}\n];\nfunction bill(){return food*(1+tip/100)}\nfunction fmt(v){return '€'+v.toFixed(2)}\nfunction render(){\n  var box=document.getElementById('ppl');box.innerHTML='';\n  var sumW=people.reduce(function(a,p){return a+p.w},0);\n  people.forEach(function(p){\n    var share=bill()*p.w/sumW,d=document.createElement('div');d.className='p';\n    d.innerHTML='<div class=\"h\"><div class=\"av\" style=\"background:'+p.c+'\">'+p.name[0]+'</div><div class=\"n\">'+p.name+'</div><div class=\"amt\">'+fmt(share)+'</div></div><div class=\"what\">'+p.what+'</div>';\n    var r=document.createElement('input');r.type='range';r.min=10;r.max=120;r.step=0.5;r.value=p.w;r.setAttribute('aria-label',p.name+' share');\n    r.oninput=function(){p.w=+r.value;update()};d.appendChild(r);box.appendChild(d);p.el=d;\n  });\n  update();\n}\nfunction update(){\n  var sumW=people.reduce(function(a,p){return a+p.w},0);\n  people.forEach(function(p){p.share=Math.round(bill()*p.w/sumW*100)/100;p.el.querySelector('.amt').textContent=fmt(p.share)});\n  document.getElementById('tot').textContent=fmt(bill());\n  document.getElementById('sub2').textContent=fmt(food)+' + '+tip+'% tip';\n}\ndocument.querySelectorAll('#tip button').forEach(function(b){b.onclick=function(){document.querySelectorAll('#tip button').forEach(function(x){x.classList.remove('on')});b.classList.add('on');tip=+b.dataset.t;update()}});\ndocument.getElementById('even').onclick=function(){people.forEach(function(p){p.w=50;p.el.querySelector('input').value=50});update()};\ndocument.getElementById('go').onclick=function(){\n  if(sent)return;sent=true;var shares={};people.forEach(function(p){shares[p.id]=p.share});\n  (window.Bridgit||window.Bridget).respond(JSON.stringify({total_eur:Math.round(bill()*100)/100,tip_percent:tip,shares_eur:shares,request_from:['maya','tom','jo']}));\n};\nrender();\n</script>\n"
}
Raw JSONhtmlslidermoney
HTML cardMoney

File this receipt as an expense?

A receipt next to a short form: category chips, client dropdown and a note. Two columns on a tablet.

Owner sees
The receipt, a warning about the policy limit, category chips, a client dropdown and a note field.
Then
submit: file it in the expense system with the receipt photo. personal: archive it and don’t ask again.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "File this receipt as an expense?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "action": "submit",
      "category": "client_meal",
      "client": "Fern & Pine Studio",
      "note": "Kick-off for the Q1 redesign",
      "amount_pln": 110
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 4.7 KB
{
  "kind": "html",
  "title": "File this receipt as an expense?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#eceff3;color:#1e2530;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:820px;margin:0 auto;padding:18px 16px 110px;display:grid;gap:16px}\n@media(min-width:760px){.wrap{grid-template-columns:1fr 1fr;align-items:start}}\nh1{font-size:22px;margin:0 0 4px;grid-column:1/-1}\n.rc{background:#fff;padding:18px 18px 22px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:14px;box-shadow:0 2px 8px rgba(0,0,0,.08);transform:rotate(-1deg);position:relative}\n.rc:after{content:\"\";position:absolute;left:0;right:0;bottom:-8px;height:8px;background:linear-gradient(-45deg,transparent 6px,#fff 6px) 0 0/12px 8px,linear-gradient(45deg,transparent 6px,#fff 6px) 0 0/12px 8px}\n.rc h2{text-align:center;font-size:16px;margin:0 0 2px}\n.rc .c{text-align:center;color:#6b7480;margin:0 0 10px;font-size:12px}\n.l{display:flex;justify-content:space-between;padding:2px 0}\n.hr{border-top:1px dashed #b8c0ca;margin:8px 0}\n.tot{font-weight:700;font-size:16px}\n.side{background:#fff;border-radius:16px;padding:16px;box-shadow:0 1px 0 #d9dee5}\n.f{margin-bottom:14px}\n.f label{display:block;font-size:13px;font-weight:600;color:#56606c;margin-bottom:6px}\n.chips{display:flex;flex-wrap:wrap;gap:8px}\n.chips button{font:inherit;font-size:14px;border:1.5px solid #cfd6df;background:#fff;border-radius:99px;padding:8px 12px;cursor:pointer}\n.chips button.on{border-color:#1a73e8;background:#e8f0fe;color:#0b57d0}\nselect,textarea{width:100%;font:inherit;border:1.5px solid #cfd6df;border-radius:10px;padding:10px;background:#fff}\ntextarea{resize:vertical;min-height:64px}\n.warn{background:#fff4e5;border-radius:10px;padding:8px 10px;font-size:14px;color:#8a4b00;margin-bottom:14px}\n.bar{position:fixed;left:0;right:0;bottom:0;background:#fff;border-top:1px solid #d9dee5;padding:12px 16px 12px}\n.bar .in{max-width:820px;margin:0 auto;display:flex;gap:10px}\n.bar button{font:inherit;font-weight:700;border:0;border-radius:12px;padding:14px;min-height:48px;cursor:pointer}\n.sub{flex:1;background:#1a73e8;color:#fff}\n.per{background:#eceff3;color:#1e2530}\n</style>\n<div class=\"wrap\">\n  <h1>File this receipt as an expense?</h1>\n  <div class=\"rc\">\n    <h2>CAFÉ NORDLYS</h2>\n    <p class=\"c\">Rynek Główny 12, Kraków · NIP 676-12-34-567<br>31.10.2026 12:48 · Table 7</p>\n    <div class=\"l\"><span>2 × Flat white</span><span>28.00</span></div>\n    <div class=\"l\"><span>1 × Smoked salmon bagel</span><span>34.00</span></div>\n    <div class=\"l\"><span>1 × Shakshuka</span><span>39.00</span></div>\n    <div class=\"l\"><span>1 × Sparkling water</span><span>9.00</span></div>\n    <div class=\"hr\"></div>\n    <div class=\"l\"><span>Net</span><span>101.85</span></div>\n    <div class=\"l\"><span>VAT 8%</span><span>8.15</span></div>\n    <div class=\"l tot\"><span>TOTAL PLN</span><span>110.00</span></div>\n    <div class=\"hr\"></div>\n    <div class=\"l\"><span>Card ****4417</span><span>APPROVED</span></div>\n  </div>\n  <div class=\"side\">\n    <div class=\"warn\">Lunch for 2 is over the 90 zł solo limit, so it needs a client name.</div>\n    <div class=\"f\"><label>Category</label><div class=\"chips\" id=\"cat\"><button data-v=\"client_meal\" class=\"on\">Client meal</button><button data-v=\"team_meal\">Team meal</button><button data-v=\"travel_meal\">Travel</button></div></div>\n    <div class=\"f\"><label for=\"client\">Client or project</label><select id=\"client\"><option value=\"\">Choose…</option><option>Harbor Logistics</option><option selected>Fern &amp; Pine Studio</option><option>Internal</option></select></div>\n    <div class=\"f\"><label for=\"note\">Note for finance (optional)</label><textarea id=\"note\" maxlength=\"300\" placeholder=\"e.g. Kick-off for the Q1 redesign\"></textarea></div>\n  </div>\n</div>\n<div class=\"bar\"><div class=\"in\"><button class=\"per\" id=\"per\">It's personal</button><button class=\"sub\" id=\"sub\">Submit 110.00 zł</button></div></div>\n<script>\nvar cat='client_meal',sent=false;\ndocument.querySelectorAll('#cat button').forEach(function(b){b.onclick=function(){document.querySelectorAll('#cat button').forEach(function(x){x.classList.remove('on')});b.classList.add('on');cat=b.dataset.v}});\nfunction send(o){if(sent)return;sent=true;(window.Bridgit||window.Bridget).respond(JSON.stringify(o))}\ndocument.getElementById('sub').onclick=function(){send({action:'submit',category:cat,client:document.getElementById('client').value||null,note:document.getElementById('note').value.trim(),amount_pln:110.00})};\ndocument.getElementById('per').onclick=function(){send({action:'personal'})};\n</script>\n"
}
Raw JSONhtmlformselecttextarea
HTML cardMoney

Parking ends in 12 minutes

A live countdown ring and three extend options. Time left is worked out from the meter's fixed end time, so it stays right if the card arrives late or is opened again. Before sending, set ENDS_AT in the script, the Ends row and the title to the meter's real end time.

Owner sees
Time left on the meter counting down, the ticket, +30 min / +1 h / +2 h and Extend.
Then
extend: pay through the parking app’s API and confirm. let_expire: do nothing.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Parking ends in 12 minutes",
  "priority": "urgent",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "action": "extend",
      "minutes": 60,
      "price_pln": "3.00",
      "seconds_left": 622
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 5.0 KB
{
  "kind": "html",
  "title": "Parking ends in 12 minutes",
  "priority": "urgent",
  "expires_in_seconds": 720,
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#0d2b45;color:#f2f6fa;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:22px 18px 28px}\n.top{display:flex;align-items:center;gap:12px;margin-bottom:18px}\n.p{width:46px;height:46px;border-radius:12px;background:#2b7fff;display:grid;place-content:center;font-weight:800;font-size:26px}\n.top b{display:block;font-size:18px}\n.top span{color:#9cb4c9;font-size:14px}\n.ring{position:relative;width:220px;height:220px;margin:6px auto 16px}\n.ring svg{width:100%;height:100%;transform:rotate(-90deg)}\n.ring .c{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}\n.ring .c b{font-size:46px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n.ring .c span{color:#9cb4c9;font-size:14px}\n.ticket{background:#f7f3e8;color:#1d2733;border-radius:14px;padding:14px 16px;margin-bottom:16px;position:relative}\n.ticket:before,.ticket:after{content:\"\";position:absolute;top:50%;width:18px;height:18px;border-radius:50%;background:#0d2b45;transform:translateY(-50%)}\n.ticket:before{left:-9px}.ticket:after{right:-9px}\n.row{display:flex;justify-content:space-between;font-size:15px;padding:3px 0}\n.row span{color:#5d6976}\n.opts{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px}\n.opt{font:inherit;border:2px solid #2d4f6e;background:#123a5c;color:#f2f6fa;border-radius:12px;padding:10px 6px;min-height:64px;cursor:pointer}\n.opt b{display:block;font-size:18px}\n.opt small{color:#9cb4c9}\n.opt.on{border-color:#2b7fff;background:#1a4f80}\nbutton.main{width:100%;font:inherit;font-weight:700;border:0;border-radius:14px;padding:16px;min-height:52px;background:#2b7fff;color:#fff;cursor:pointer;margin-bottom:8px}\nbutton.alt{width:100%;font:inherit;border:0;border-radius:14px;padding:14px;background:transparent;color:#9cb4c9;cursor:pointer}\n</style>\n<div class=\"wrap\">\n  <div class=\"top\"><div class=\"p\">P</div><div><b>Parking ends soon</b><span>Zone 2214 · Kazimierz, ul. Józefa</span></div></div>\n  <div class=\"ring\"><svg viewBox=\"0 0 120 120\"><circle cx=\"60\" cy=\"60\" r=\"52\" fill=\"none\" stroke=\"#1d4366\" stroke-width=\"10\"/><circle id=\"arc\" cx=\"60\" cy=\"60\" r=\"52\" fill=\"none\" stroke=\"#ffb020\" stroke-width=\"10\" stroke-linecap=\"round\" stroke-dasharray=\"326.7\" stroke-dashoffset=\"0\"/></svg>\n    <div class=\"c\"><b id=\"t\">12:00</b><span id=\"u\">left on the meter</span></div></div>\n  <div class=\"ticket\">\n    <div class=\"row\"><span>Car</span><b>KR 7H221 · blue Golf</b></div>\n    <div class=\"row\"><span>Started</span><b>13:05</b></div>\n    <div class=\"row\"><span>Ends</span><b>15:05</b></div>\n    <div class=\"row\"><span>Paid so far</span><b>6.00 zł</b></div>\n    <div class=\"row\"><span>Rate</span><b>3.00 zł / hour</b></div>\n  </div>\n  <div class=\"opts\">\n    <button class=\"opt\" data-m=\"30\"><b>+30 min</b><small>1.50 zł</small></button>\n    <button class=\"opt on\" data-m=\"60\"><b>+1 h</b><small>3.00 zł</small></button>\n    <button class=\"opt\" data-m=\"120\"><b>+2 h</b><small>6.00 zł</small></button>\n  </div>\n  <button class=\"main\" id=\"ext\">Extend 1 h for 3.00 zł</button>\n  <button class=\"alt\" id=\"end\">I'm leaving, let it run out</button>\n</div>\n<script>\n// The meter's fixed end time, set by the agent when it sends the card (here 13:05 + 2 h paid). Time left is\n// always worked out from it, so the card stays right if it arrives late or is opened again.\nvar ENDS_AT=Date.parse('2026-11-02T15:05:00+01:00'),TOTAL=12*60;\nif(window.BridgitGallery)ENDS_AT=Date.now()+TOTAL*1000; // the website's preview only\nvar mins=60,sent=false;\nfunction left(){return Math.max(0,Math.round((ENDS_AT-Date.now())/1000))}\nfunction fmt(s){var m=Math.floor(s/60),x=s%60;return m+':'+(x<10?'0':'')+x}\nfunction tick(){\n  var l=left();\n  document.getElementById('t').textContent=fmt(l);\n  document.getElementById('arc').setAttribute('stroke-dashoffset',String(326.7*(1-Math.min(l,TOTAL)/TOTAL)));\n  if(l<=300)document.getElementById('arc').setAttribute('stroke','#ff5a4f');\n  if(l===0)document.getElementById('u').textContent='the meter has run out';\n}\ntick();setInterval(tick,1000);\ndocument.querySelectorAll('.opt').forEach(function(b){b.onclick=function(){\n  document.querySelectorAll('.opt').forEach(function(x){x.classList.remove('on')});b.classList.add('on');mins=+b.dataset.m;\n  document.getElementById('ext').textContent='Extend '+b.querySelector('b').textContent.replace('+','')+' for '+b.querySelector('small').textContent;\n}});\nfunction send(o){if(sent)return;sent=true;o.seconds_left=left();(window.Bridgit||window.Bridget).respond(JSON.stringify(o))}\ndocument.getElementById('ext').onclick=function(){send({action:'extend',minutes:mins,price_pln:(mins/60*3).toFixed(2)})};\ndocument.getElementById('end').onclick=function(){send({action:'let_expire'})};\n</script>\n"
}
Raw JSONhtmltimerlive
HTML cardMoney

October spending: €2,184 of €2,400

A monthly budget review: a donut chart, a meter per category and three fix-it choices, so the owner sees the whole month before agreeing to a change.

Owner sees
A donut of October spending with the total in the middle, six category rows with Groceries flagged €46 over, and three choices with the agent's pick selected.
Then
move: shift €50 from Eating out to Groceries for this month. raise: set the Groceries budget to €470. leave: change nothing and mention it again in the end-of-month summary.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "October spending: €2,184 of €2,400",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "action": "move",
      "from": "eating_out",
      "to": "groceries",
      "amount_eur": 50
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 10.3 KB
{
  "kind": "html",
  "title": "October spending: €2,184 of €2,400",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:radial-gradient(120% 60% at 50% 0%,#164336 0%,#0f2e25 60%);background-color:#0f2e25;color:#eaf6f0;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:13px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#5ee6a8;margin:0 0 4px}\nh1{font-size:22px;line-height:1.25;margin:0 0 4px;letter-spacing:-.01em}\nh1.done{color:#5ee6a8}\n.sub{color:#a9c7bb;font-size:15px;margin:0 0 18px}\n.chart{position:relative;width:212px;height:212px;margin:0 auto 20px}\n.chart svg{width:100%;height:100%;transform:rotate(-90deg)}\n.chart circle{transition:stroke-dasharray .2s,stroke-dashoffset .2s}\n.mid{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}\n.mid b{font-size:32px;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums}\n.mid span{font-size:14px;color:#a9c7bb;font-variant-numeric:tabular-nums}\n.mid em{font-style:normal;font-size:13px;font-weight:600;color:#5ee6a8;margin-top:4px;font-variant-numeric:tabular-nums}\n.list{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.07);border-radius:18px;padding:4px 16px;margin-bottom:18px}\n.row{padding:12px 0;border-bottom:1px solid rgba(255,255,255,.07)}\n.row:last-child{border-bottom:0}\n.rh{display:flex;align-items:center;gap:10px;margin-bottom:8px}\n.dot{width:10px;height:10px;border-radius:50%;flex:none}\n.nm{flex:1;font-weight:600;font-size:15px}\n.amt{font-size:14px;color:#a9c7bb;font-variant-numeric:tabular-nums}\n.amt b{color:#eaf6f0;font-weight:600}\n.amt i{font-style:normal;font-weight:700;color:#5ee6a8;margin-left:6px}\n.amt i:empty{display:none}\n.over .amt b,.flag{color:#ff7a6b}\n.flag{display:block;font-size:13px;font-weight:600;margin-top:6px}\n.flag:empty{display:none}\n.meter{height:6px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden}\n.fill{height:100%;border-radius:3px;transition:width .2s,background-color .2s}\n.ask{background:rgba(94,230,168,.08);border:1px solid rgba(94,230,168,.22);border-radius:16px;padding:14px 16px;margin-bottom:12px;font-size:15px;color:#cfe8dd}\n.ask b{color:#eaf6f0}\n.chips{display:flex;flex-direction:column;gap:8px;margin-bottom:18px}\n.chip{display:flex;align-items:center;gap:10px;width:100%;min-height:52px;font:inherit;font-size:15px;text-align:left;color:#eaf6f0;background:rgba(255,255,255,.04);border:1.5px solid rgba(255,255,255,.12);border-radius:14px;padding:10px 14px;cursor:pointer;transition:background-color .18s,border-color .18s,transform .15s}\n.chip:active,.main:active{transform:scale(.98)}\n.chip .r{width:20px;height:20px;border-radius:50%;border:2px solid #6f9586;flex:none;transition:border .18s}\n.chip .t{flex:1}\n.chip small{font-size:12px;font-weight:700;color:#0f2e25;background:#5ee6a8;border-radius:20px;padding:2px 8px}\n.chip.on{border-color:#5ee6a8;background:rgba(94,230,168,.12)}\n.chip.on .r{border:6px solid #5ee6a8}\n.main{width:100%;min-height:54px;font:inherit;font-weight:700;font-size:16px;border:0;border-radius:16px;background:#5ee6a8;color:#0b2a20;cursor:pointer;box-shadow:0 8px 24px rgba(94,230,168,.22);transition:transform .15s,opacity .2s}\n.main:disabled,.chip:disabled{opacity:.55;cursor:default}\n.status{text-align:center;font-size:14px;color:#a9c7bb;min-height:20px;margin:10px 0 0}\n</style>\n<div class=\"wrap\">\n  <p class=\"eyebrow\" id=\"eyebrow\"></p>\n  <h1 id=\"title\"></h1>\n  <p class=\"sub\" id=\"sub\"></p>\n  <div class=\"chart\"><svg viewBox=\"0 0 120 120\" id=\"donut\"><circle cx=\"60\" cy=\"60\" r=\"50\" fill=\"none\" stroke=\"rgba(255,255,255,.06)\" stroke-width=\"12\"/></svg>\n    <div class=\"mid\"><b id=\"spent\"></b><span id=\"of\"></span><em id=\"left\"></em></div></div>\n  <div class=\"list\" id=\"list\"></div>\n  <div class=\"ask\" id=\"ask\"></div>\n  <div class=\"chips\" id=\"chips\"></div>\n  <button class=\"main\" id=\"go\"></button>\n  <p class=\"status\" id=\"status\" role=\"status\"></p>\n</div>\n<script>\n// The card's content. Change only this object.\n// answer.data: {\"action\":\"move\"|\"raise\"|\"leave\",\"from\":\"eating_out\",\"to\":\"groceries\",\"amount_eur\":50}\nvar CARD = {\n  eyebrow: 'October budget',\n  title: 'October spending: €2,184 of €2,400',\n  sub: 'Six days left. Groceries went over; everything else is on track.',\n  currency: '€',\n  categories: [\n    {id: 'home', name: 'Home & bills', spent: 1120, budget: 1150, color: '#7fb7d9'},\n    {id: 'groceries', name: 'Groceries', spent: 466, budget: 420, color: '#e8c468'},\n    {id: 'eating_out', name: 'Eating out', spent: 148, budget: 240, color: '#c49cf0'},\n    {id: 'transport', name: 'Transport', spent: 162, budget: 200, color: '#6fd3c7'},\n    {id: 'fun', name: 'Fun & hobbies', spent: 178, budget: 230, color: '#f2a6c0'},\n    {id: 'health', name: 'Health', spent: 110, budget: 160, color: '#a7cf8c'}\n  ],\n  over_label: 'over budget so far',\n  of_label: 'of',\n  left_label: 'left',\n  suggestion: 'Groceries is €46 over. Eating out still has €92 spare, so I suggest moving €50 across.',\n  from: 'eating_out', to: 'groceries', amount: 50, raise_to: 470,\n  choices: [\n    {action: 'move', label: 'Move €50 from Eating out', badge: 'My pick', button: 'Apply · move €50'},\n    {action: 'raise', label: 'Raise Groceries budget to €470', button: 'Apply · raise to €470'},\n    {action: 'leave', label: 'Leave it as it is', button: 'Leave budgets as they are'}\n  ],\n  sending: 'Sending…',\n  preview: 'Preview only: open this card in Bridgit to answer.',\n  done: 'Budget updated for October'\n};\n\nvar choice = CARD.choices[0], sent = false, C = 2 * Math.PI * 50;\nfunction $(id) { return document.getElementById(id); }\nfunction eur(v) { return CARD.currency + v.toLocaleString('en-GB'); }\n$('eyebrow').textContent = CARD.eyebrow;\n$('title').textContent = CARD.title;\n$('sub').textContent = CARD.sub;\n$('ask').textContent = CARD.suggestion;\n\n// Donut\nvar svg = $('donut'), NS = 'http://www.w3.org/2000/svg';\nvar total = CARD.categories.reduce(function (a, c) { return a + c.spent; }, 0), off = 0, gap = 2.2;\nCARD.categories.forEach(function (c) {\n  var len = C * c.spent / total, el = document.createElementNS(NS, 'circle');\n  el.setAttribute('cx', 60); el.setAttribute('cy', 60); el.setAttribute('r', 50);\n  el.setAttribute('fill', 'none'); el.setAttribute('stroke', c.color); el.setAttribute('stroke-width', 12);\n  el.setAttribute('stroke-dasharray', Math.max(len - gap, 0.5) + ' ' + C);\n  el.setAttribute('stroke-dashoffset', -off);\n  svg.appendChild(el); off += len;\n});\n\n// Rows\nvar rows = {};\nCARD.categories.forEach(function (c) {\n  var r = document.createElement('div'); r.className = 'row';\n  var h = document.createElement('div'); h.className = 'rh';\n  var d = document.createElement('span'); d.className = 'dot'; d.style.background = c.color;\n  var n = document.createElement('span'); n.className = 'nm'; n.textContent = c.name;\n  var f = document.createElement('span'); f.className = 'flag';\n  var a = document.createElement('span'); a.className = 'amt';\n  var b = document.createElement('b'); var s = document.createElement('span'); var t = document.createElement('i');\n  a.appendChild(b); a.appendChild(s); a.appendChild(t); h.appendChild(d); h.appendChild(n); h.appendChild(a);\n  var m = document.createElement('div'); m.className = 'meter';\n  var fl = document.createElement('div'); fl.className = 'fill'; m.appendChild(fl);\n  r.appendChild(h); r.appendChild(m); r.appendChild(f); $('list').appendChild(r);\n  rows[c.id] = {row: r, b: b, s: s, t: t, f: f, fill: fl, c: c};\n});\n\nfunction budgetOf(c) {\n  if (choice.action === 'move' && c.id === CARD.from) return c.budget - CARD.amount;\n  if (choice.action === 'move' && c.id === CARD.to) return c.budget + CARD.amount;\n  if (choice.action === 'raise' && c.id === CARD.to) return CARD.raise_to;\n  return c.budget;\n}\nfunction render() {\n  var budget = 0;\n  CARD.categories.forEach(function (c) {\n    var R = rows[c.id], bud = budgetOf(c), over = c.spent > c.budget;\n    budget += bud;\n    R.b.textContent = eur(c.spent); R.s.textContent = ' / ' + eur(c.budget);\n    R.t.textContent = bud !== c.budget ? '→ ' + eur(bud) : '';\n    R.f.textContent = over ? eur(c.spent - c.budget) + ' ' + CARD.over_label : '';\n    R.row.classList.toggle('over', over);\n    R.fill.style.width = Math.min(100, c.spent / c.budget * 100) + '%';\n    R.fill.style.backgroundColor = over ? '#ff7a6b' : c.color;\n  });\n  $('spent').textContent = eur(total);\n  $('of').textContent = CARD.of_label + ' ' + eur(budget);\n  $('left').textContent = eur(budget - total) + ' ' + CARD.left_label;\n  $('go').textContent = choice.button;\n}\n\n// Choices\nCARD.choices.forEach(function (ch, i) {\n  var b = document.createElement('button'); b.className = 'chip' + (i === 0 ? ' on' : '');\n  var r = document.createElement('span'); r.className = 'r';\n  var t = document.createElement('span'); t.className = 't'; t.textContent = ch.label;\n  b.appendChild(r); b.appendChild(t);\n  if (ch.badge) { var s = document.createElement('small'); s.textContent = ch.badge; b.appendChild(s); }\n  b.onclick = function () {\n    if (sent) return; choice = ch;\n    document.querySelectorAll('.chip').forEach(function (x) { x.classList.remove('on'); });\n    b.classList.add('on'); render();\n  };\n  $('chips').appendChild(b);\n});\n\n$('go').onclick = function () {\n  if (sent) return;\n  var B = window.Bridgit || window.Bridget;\n  if (!B) { $('status').textContent = CARD.preview; return; }\n  var data = {action: choice.action};\n  if (choice.action === 'move') { data.from = CARD.from; data.to = CARD.to; data.amount_eur = CARD.amount; }\n  if (choice.action === 'raise') { data.category = CARD.to; data.new_budget_eur = CARD.raise_to; }\n  sent = true;\n  document.querySelectorAll('button').forEach(function (x) { x.disabled = true; });\n  $('status').textContent = CARD.sending;\n  B.respond(JSON.stringify(data));\n};\ndocument.addEventListener('bridgit:answered', function (e) {\n  e.preventDefault();\n  $('title').textContent = '✓ ' + CARD.done; $('title').classList.add('done');\n  $('status').textContent = '';\n  setTimeout(function () { $('title').scrollIntoView({block: 'center', behavior: 'smooth'}); }, 400);\n});\nrender();\n</script>\n",
  "expires_in_seconds": 86400
}
Raw JSONhtmlchartbudgetmoney
HTML cardMoney

7 subscriptions, €94.93 a month. Keep them all?

A list of subscriptions with Keep/Cancel switches and a live savings counter, with the unused ones switched off for the owner already.

Owner sees
A big "You'd save €312 a year" counter, seven subscriptions with price, when each was last used and a switch, and a Cancel button that counts them.
Then
Cancel each listed subscription (or open its cancel page if it can't be done for the owner) and confirm the end dates. If cancel is empty, keep them all and don't ask again for three months.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "7 subscriptions, €94.93 a month. Keep them all?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "cancel": [
        "streambox",
        "fitplus",
        "lingualeap"
      ],
      "keep": [
        "cloudvault",
        "courier",
        "musichub",
        "pixelstudio"
      ],
      "saves_eur_month": 26,
      "saves_eur_year": 312
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 8.9 KB
{
  "kind": "html",
  "title": "7 subscriptions, €94.93 a month. Keep them all?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#111318;color:#f1f3f6;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\nh1{font-size:21px;line-height:1.3;margin:0 0 4px;letter-spacing:-.01em}\nh1.done{color:#c6f432}\n.sub{color:#9aa1ad;font-size:15px;margin:0 0 16px}\n.save{position:relative;overflow:hidden;background:linear-gradient(140deg,#1d2210,#171a20 70%);border:1px solid rgba(198,244,50,.25);border-radius:20px;padding:18px 18px 16px;margin-bottom:18px}\n.save span{display:block;font-size:14px;color:#b7bdc7}\n.save b{display:block;font-size:40px;font-weight:800;letter-spacing:-.03em;color:#c6f432;font-variant-numeric:tabular-nums;line-height:1.15;transition:color .2s}\n.save b.zero{color:#6b7280}\n.save small{font-size:14px;color:#9aa1ad;font-variant-numeric:tabular-nums}\n.list{display:flex;flex-direction:column;gap:8px;margin-bottom:18px}\n.sub-row{display:flex;align-items:center;gap:10px;background:#1b1e25;border:1px solid #252a33;border-radius:16px;padding:12px 12px 12px 12px;min-height:68px;cursor:pointer;transition:background-color .18s,border-color .18s,transform .15s}\n.sub-row:active{transform:scale(.98)}\n.sub-row.cut{background:#15171c;border-color:rgba(198,244,50,.35)}\n.av{width:42px;height:42px;border-radius:12px;display:grid;place-content:center;font-weight:800;font-size:18px;color:#fff;flex:none;transition:opacity .18s}\n.cut .av{opacity:.45}\n.info{flex:1;min-width:0}\n.nm{display:block;font-weight:600;font-size:15px;line-height:1.3}\n.hint{display:block;font-size:13px;color:#9aa1ad}\n.hint.idle{color:#f5b85a}\n.pr{font-weight:600;font-size:15px;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}\n.pr small{display:block;font-size:12px;font-weight:500;color:#8b929e}\n.cut .pr{color:#7d8491}\n.cut .pr span{text-decoration:line-through}\n.sw{position:relative;width:52px;height:32px;border-radius:16px;background:#3a3f4a;flex:none;transition:background-color .2s}\n.sw:after{content:\"\";position:absolute;top:3px;left:3px;width:26px;height:26px;border-radius:50%;background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.35);transition:transform .2s}\n.sw.on{background:#c6f432}\n.sw.on:after{transform:translateX(20px)}\n.sl{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;text-align:center;margin-top:3px;color:#9aa1ad}\n.cut .sl{color:#c6f432}\n.main{width:100%;min-height:54px;font:inherit;font-weight:700;font-size:16px;border:0;border-radius:16px;background:#c6f432;color:#14170c;cursor:pointer;box-shadow:0 8px 24px rgba(198,244,50,.18);transition:transform .15s,opacity .2s}\n.main:active,.alt:active{transform:scale(.98)}\n.main:disabled{opacity:.4;cursor:default;box-shadow:none}\n.alt{width:100%;min-height:48px;font:inherit;font-size:15px;border:0;background:none;color:#b7bdc7;cursor:pointer;margin-top:6px;border-radius:12px}\n.alt:disabled{opacity:.5}\n.status{text-align:center;font-size:14px;color:#9aa1ad;min-height:20px;margin:6px 0 0}\n</style>\n<div class=\"wrap\">\n  <h1 id=\"title\"></h1>\n  <p class=\"sub\" id=\"sub\"></p>\n  <div class=\"save\"><span id=\"saveLead\"></span><b id=\"year\"></b><small id=\"month\"></small></div>\n  <div class=\"list\" id=\"list\"></div>\n  <button class=\"main\" id=\"go\"></button>\n  <button class=\"alt\" id=\"keep\"></button>\n  <p class=\"status\" id=\"status\" role=\"status\"></p>\n</div>\n<script>\n// The card's content. Change only this object.\n// answer.data: {\"cancel\":[\"streambox\",…],\"keep\":[\"cloudvault\",…],\"saves_eur_month\":26.0,\"saves_eur_year\":312}\nvar CARD = {\n  title: '7 subscriptions, €94.93 a month. Keep them all?',\n  sub: 'I checked your card statement. Three of these haven’t been opened in over a month.',\n  currency: '€',\n  save_lead: 'You’d save',\n  idle_after_days: 30,\n  subs: [\n    {id: 'streambox', name: 'StreamBox', price: 13.50, last: 'Unused for 63 days', days: 63, color: '#e5484d'},\n    {id: 'cloudvault', name: 'CloudVault 2 TB', price: 9.96, last: 'Synced today', days: 0, color: '#3e7bfa'},\n    {id: 'fitplus', name: 'Fit+ Gym app', price: 4.50, last: 'Unused for 41 days', days: 41, color: '#f76b15'},\n    {id: 'courier', name: 'Daily Courier news', price: 14.99, last: 'Read yesterday', days: 1, color: '#5b5bd6'},\n    {id: 'musichub', name: 'MusicHub Family', price: 19.99, last: 'Used today', days: 0, color: '#12a594'},\n    {id: 'pixelstudio', name: 'Pixel Studio', price: 23.99, last: 'Used 3 days ago', days: 3, color: '#d6409f'},\n    {id: 'lingualeap', name: 'LinguaLeap', price: 8.00, last: 'Unused for 108 days', days: 108, color: '#30a46c'}\n  ],\n  per_month: '/mo',\n  keep_word: 'Keep', cancel_word: 'Cancel',\n  keep_all: 'Keep them all',\n  none_selected: 'Switch off any you want to cancel',\n  sending: 'Sending…',\n  preview: 'Preview only: open this card in Bridgit to answer.',\n  done: 'Got it. I’ll cancel those for you',\n  done_keep: 'Got it. Keeping all seven'\n};\n\nvar sent = false, state = {}, doneText = CARD.done;\nfunction $(id) { return document.getElementById(id); }\nfunction money(v) { return CARD.currency + v.toFixed(2); }\n$('title').textContent = CARD.title;\n$('sub').textContent = CARD.sub;\n$('saveLead').textContent = CARD.save_lead;\n$('keep').textContent = CARD.keep_all;\n\nCARD.subs.forEach(function (s) {\n  state[s.id] = s.days < CARD.idle_after_days; // true = keep\n  var r = document.createElement('div'); r.className = 'sub-row'; r.setAttribute('role', 'switch'); r.tabIndex = 0;\n  var av = document.createElement('div'); av.className = 'av'; av.style.background = s.color; av.textContent = s.name.charAt(0);\n  var info = document.createElement('div'); info.className = 'info';\n  var nm = document.createElement('span'); nm.className = 'nm'; nm.textContent = s.name;\n  var h = document.createElement('span'); h.className = 'hint' + (s.days >= CARD.idle_after_days ? ' idle' : ''); h.textContent = s.last;\n  info.appendChild(nm); info.appendChild(h);\n  var pr = document.createElement('div'); pr.className = 'pr'; var pv = document.createElement('span'); pv.textContent = money(s.price); pr.appendChild(pv);\n  var pm = document.createElement('small'); pm.textContent = CARD.per_month; pr.appendChild(pm);\n  var wrap = document.createElement('div');\n  var sw = document.createElement('div'); sw.className = 'sw';\n  var sl = document.createElement('span'); sl.className = 'sl';\n  wrap.appendChild(sw); wrap.appendChild(sl);\n  r.appendChild(av); r.appendChild(info); r.appendChild(pr); r.appendChild(wrap);\n  r.onclick = function () { if (sent) return; state[s.id] = !state[s.id]; update(); };\n  r.onkeydown = function (e) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); r.onclick(); } };\n  s.el = {row: r, sw: sw, sl: sl};\n  $('list').appendChild(r);\n});\n\nfunction totals() {\n  var cut = CARD.subs.filter(function (s) { return !state[s.id]; });\n  var m = Math.round(cut.reduce(function (a, s) { return a + s.price; }, 0) * 100) / 100;\n  return {cut: cut, month: m, year: Math.round(m * 12)};\n}\nfunction update() {\n  CARD.subs.forEach(function (s) {\n    var keep = state[s.id];\n    s.el.row.classList.toggle('cut', !keep);\n    s.el.sw.classList.toggle('on', keep);\n    s.el.sl.textContent = keep ? CARD.keep_word : CARD.cancel_word;\n    s.el.row.setAttribute('aria-checked', String(keep));\n    s.el.row.setAttribute('aria-label', s.name + ', ' + (keep ? CARD.keep_word : CARD.cancel_word));\n  });\n  var t = totals(), n = t.cut.length;\n  $('year').textContent = CARD.currency + t.year + ' a year';\n  $('year').classList.toggle('zero', n === 0);\n  $('month').textContent = money(t.month) + ' a month · ' + n + ' to cancel';\n  $('go').textContent = n ? 'Cancel ' + n + ' subscription' + (n > 1 ? 's' : '') : CARD.none_selected;\n  $('go').disabled = n === 0;\n}\n\nfunction send(data) {\n  if (sent) return;\n  var B = window.Bridgit || window.Bridget;\n  if (!B) { $('status').textContent = CARD.preview; return; }\n  sent = true;\n  $('go').disabled = true; $('keep').disabled = true;\n  $('status').textContent = CARD.sending;\n  B.respond(JSON.stringify(data));\n}\n$('go').onclick = function () {\n  var t = totals();\n  send({\n    cancel: t.cut.map(function (s) { return s.id; }),\n    keep: CARD.subs.filter(function (s) { return state[s.id]; }).map(function (s) { return s.id; }),\n    saves_eur_month: t.month, saves_eur_year: t.year\n  });\n};\n$('keep').onclick = function () {\n  doneText = CARD.done_keep;\n  send({cancel: [], keep: CARD.subs.map(function (s) { return s.id; }), saves_eur_month: 0, saves_eur_year: 0});\n};\ndocument.addEventListener('bridgit:answered', function (e) {\n  e.preventDefault();\n  $('title').textContent = '✓ ' + doneText; $('title').classList.add('done');\n  $('status').textContent = '';\n  setTimeout(function () { $('title').scrollIntoView({block: 'center', behavior: 'smooth'}); }, 400);\n});\nupdate();\n</script>\n",
  "expires_in_seconds": 86400
}
Raw JSONhtmlswitchsubscriptionsmoney

Plans

Your agent
Book Osteria Lupa for Saturday?

They have these tables free on Sat 8 Nov. 4.7★, 12 min walk from you.

Time
How many?
View the menu ↗Opens example.com
QuestionPlans

Book Osteria Lupa for Saturday?

Choice plus an optional note, and a link to the menu. One card replaces a back-and-forth.

Owner sees
A time choice, a party size choice, a notes field, Book it, and a View the menu link.
Then
Book that slot and send a notice with the confirmation.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "Book Osteria Lupa for Saturday?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "book",
    "values": {
      "time": "19:30",
      "party": "4",
      "notes": "One vegetarian, a high chair please"
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.9 KB
{
  "kind": "question",
  "title": "Book Osteria Lupa for Saturday?",
  "text": "They have these tables free on Sat 8 Nov. 4.7★, 12 min walk from you.",
  "controls": [
    {
      "type": "choice",
      "id": "time",
      "label": "Time",
      "options": [
        "18:00",
        "19:30",
        "21:15"
      ]
    },
    {
      "type": "choice",
      "id": "party",
      "label": "How many?",
      "options": [
        "2",
        "3",
        "4",
        "5",
        "6"
      ]
    },
    {
      "type": "text",
      "id": "notes",
      "label": "Anything they should know?",
      "placeholder": "Allergies, high chair, birthday"
    },
    {
      "type": "button",
      "id": "book",
      "label": "Book it",
      "style": "primary"
    },
    {
      "type": "link",
      "id": "menu",
      "label": "View the menu",
      "url": "https://example.com/osteria-lupa/menu"
    }
  ]
}
Raw JSONchoicetextlink
Your agent
Dentist check-up: which slot?

Dr. Nowak has three openings next week. All 30 minutes, 10 min by tram. Your calendar is free for each.

QuestionPlans

Dentist check-up: which slot?

Three buttons, one per slot, plus a way out. The fastest scheduling there is.

Owner sees
Three time buttons and None of these.
Then
Send the invite for that slot. none: ask what works instead with a text card.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "Dentist check-up: which slot?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "thu_1000",
    "values": {}
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.6 KB
{
  "kind": "question",
  "title": "Dentist check-up: which slot?",
  "text": "Dr. Nowak has three openings next week. All 30 minutes, 10 min by tram. Your calendar is free for each.",
  "controls": [
    {
      "type": "button",
      "id": "tue_0830",
      "label": "Tue 11 Nov, 8:30"
    },
    {
      "type": "button",
      "id": "thu_1000",
      "label": "Thu 13 Nov, 10:00",
      "style": "primary"
    },
    {
      "type": "button",
      "id": "fri_1630",
      "label": "Fri 14 Nov, 16:30"
    },
    {
      "type": "button",
      "id": "none",
      "label": "None of these"
    }
  ]
}
Raw JSONbuttonscalendar
HTML cardPlans

Pick seats for Orbit Hearts, Fri 19:40

A tappable seat map with taken seats, best-view hints and a running total.

Owner sees
The screen, rows A to H, taken seats crossed out, and Book 2 seats.
Then
Buy those seats and send the tickets as a notice with a link.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Pick seats for Orbit Hearts, Fri 19:40",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "seats": [
        "E7",
        "E8"
      ],
      "total_pln": 64,
      "showing": "2026-11-07T19:40"
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 4.4 KB
{
  "kind": "html",
  "title": "Pick seats for Orbit Hearts, Fri 19:40",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#15111f;color:#f3effa;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:640px;margin:0 auto;padding:20px 14px 120px}\n.film{display:flex;gap:12px;align-items:center;margin-bottom:16px}\n.poster{width:54px;height:76px;border-radius:8px;background:linear-gradient(160deg,#ff6b6b,#5f27cd);display:grid;place-content:center;font-size:26px;flex:none}\n.film b{display:block;font-size:19px}\n.film span{color:#a99fc0;font-size:14px}\n.screen{height:26px;margin:8px 8% 22px;border-top:4px solid #b9a6ff;border-radius:50% 50% 0 0/100% 100% 0 0;text-align:center;font-size:12px;letter-spacing:.3em;color:#8e84a8;padding-top:6px;box-shadow:0 -10px 24px -10px #b9a6ff}\n.hall{display:grid;gap:6px;justify-content:center}\n.r{display:flex;gap:min(6px,1.2vw);align-items:center}\n.r em{width:16px;font-style:normal;font-size:12px;color:#8e84a8;text-align:center}\n.s{width:min(34px,calc((100vw - 150px) / 10));height:min(34px,calc((100vw - 150px) / 10));border-radius:8px 8px 4px 4px;border:0;background:#3a3150;padding:0;cursor:pointer}\n.s.gap{margin-left:min(18px,3vw)}\n.s.taken{background:#2a2438;cursor:not-allowed;background-image:linear-gradient(45deg,transparent 45%,#45395e 45%,#45395e 55%,transparent 55%)}\n.s.on{background:#b9a6ff;box-shadow:0 0 0 2px #fff inset}\n.s.best{outline:2px dashed #ffcf5c;outline-offset:1px}\n.legend{display:flex;justify-content:center;gap:16px;margin:16px 0 0;font-size:13px;color:#a99fc0}\n.legend i{display:inline-block;width:14px;height:14px;border-radius:4px;vertical-align:-2px;margin-right:5px}\n.bar{position:fixed;left:0;right:0;bottom:0;background:#1d1729;border-top:1px solid #2f2742;padding:12px 16px 12px}\n.bar .in{max-width:640px;margin:0 auto;display:flex;gap:12px;align-items:center}\n.bar p{flex:1;min-width:0;margin:0;font-size:14px;color:#a99fc0}\n.bar p b{color:#f3effa;display:block;font-size:16px}\n.go{font:inherit;font-weight:700;border:0;border-radius:12px;padding:14px 18px;min-height:48px;background:#b9a6ff;color:#1b1430;cursor:pointer}\n.go:disabled{opacity:.35}\n</style>\n<div class=\"wrap\">\n  <div class=\"film\"><div class=\"poster\">🚀</div><div><b>Orbit Hearts</b><span>Fri 7 Nov · 19:40 · Hall 3 · Pick 2 seats</span></div></div>\n  <div class=\"screen\">SCREEN</div>\n  <div class=\"hall\" id=\"hall\"></div>\n  <div class=\"legend\"><span><i style=\"background:#3a3150\"></i>Free</span><span><i style=\"background:#b9a6ff\"></i>Yours</span><span><i style=\"outline:2px dashed #ffcf5c;background:#3a3150\"></i>Best view</span></div>\n</div>\n<div class=\"bar\"><div class=\"in\"><p id=\"sum\"><b>No seats yet</b>Tap 2 seats</p><button class=\"go\" id=\"go\" disabled>Book 2 seats</button></div></div>\n<script>\nvar rows='ABCDEFGH',taken={A3:1,A4:1,B5:1,B6:1,C2:1,D7:1,D8:1,E4:1,E5:1,E6:1,F1:1,F10:1,G3:1,G4:1,H5:1,H6:1,H7:1},best={E7:1,E8:1,D5:1,D6:1},picked=[],need=2,sent=false;\nvar hall=document.getElementById('hall');\nrows.split('').forEach(function(r){\n  var row=document.createElement('div');row.className='r';row.innerHTML='<em>'+r+'</em>';\n  for(var n=1;n<=10;n++){\n    var id=r+n,b=document.createElement('button');b.className='s'+(n===6?' gap':'')+(taken[id]?' taken':'')+(best[id]?' best':'');\n    b.setAttribute('aria-label','Row '+r+' seat '+n+(taken[id]?' taken':''));b.dataset.id=id;if(taken[id])b.disabled=true;\n    b.onclick=function(){toggle(this)};row.appendChild(b);\n  }\n  row.insertAdjacentHTML('beforeend','<em>'+r+'</em>');hall.appendChild(row);\n});\nfunction toggle(b){\n  var id=b.dataset.id,k=picked.indexOf(id);\n  if(k>=0){picked.splice(k,1);b.classList.remove('on')}\n  else{if(picked.length>=need){var old=picked.shift();document.querySelector('[data-id=\"'+old+'\"]').classList.remove('on')}picked.push(id);b.classList.add('on')}\n  var s=document.getElementById('sum'),g=document.getElementById('go');\n  s.innerHTML=picked.length?'<b>'+picked.join(', ')+'</b>'+(picked.length*32)+' zł · '+(picked.length===need?'ready':'pick '+(need-picked.length)+' more'):'<b>No seats yet</b>Tap 2 seats';\n  g.disabled=picked.length!==need;\n}\ndocument.getElementById('go').onclick=function(){if(sent)return;sent=true;(window.Bridgit||window.Bridget).respond(JSON.stringify({seats:picked.slice().sort(),total_pln:picked.length*32,showing:'2026-11-07T19:40'}))};\n</script>\n"
}
Raw JSONhtmlgridpicker
Your agent🔊 Read aloud
Leave in 10 minutes for the airport

Traffic on the A4 is building. Leaving at 14:20 gets you to KRK by 15:05, 1 h 35 min before your flight.

Open directions ↗Opens the map
Dismiss
NoticePlans

Leave in 10 minutes for the airport

A spoken heads-up with a map link. No answer needed.

Owner sees
A notice, read aloud, with an Open directions link.
Then
Nothing to wait for. Check traffic again in 5 minutes if they haven’t moved.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "notice",
  "status": "seen",
  "title": "Leave in 10 minutes for the airport",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": null,
  "answered_at": null,
  "closed_at": null,
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.4 KB
{
  "kind": "notice",
  "title": "Leave in 10 minutes for the airport",
  "text": "Traffic on the A4 is building. Leaving at 14:20 gets you to KRK by 15:05, 1 h 35 min before your flight.",
  "speak": true,
  "controls": [
    {
      "type": "link",
      "id": "directions",
      "label": "Open directions",
      "url": "geo:0,0?q=Krak%C3%B3w+Airport"
    }
  ]
}
Raw JSONnoticespeakgeo
Your agentUrgent🔊 Read aloud
Gate change: FR 2417 now leaves from B14

Moved from A3. B14 is a 9 minute walk; boarding starts 16:10. Seat 14C is unchanged.

Boarding pass ↗Opens example.com
Dismiss
NoticePlans

Gate change: FR 2417 now leaves from B14

An urgent notice with the boarding pass a tap away.

Owner sees
An urgent notice, read aloud, with a Boarding pass link.
Then
Nothing to wait for. Watch for further changes.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "notice",
  "status": "seen",
  "title": "Gate change: FR 2417 now leaves from B14",
  "priority": "urgent",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": null,
  "answered_at": null,
  "closed_at": null,
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.4 KB
{
  "kind": "notice",
  "title": "Gate change: FR 2417 now leaves from B14",
  "text": "Moved from A3. B14 is a 9 minute walk; boarding starts 16:10. Seat 14C is unchanged.",
  "priority": "urgent",
  "speak": true,
  "controls": [
    {
      "type": "link",
      "id": "pass",
      "label": "Boarding pass",
      "url": "https://example.com/boarding/FR2417"
    }
  ]
}
Raw JSONnoticeurgentspeaklink
HTML cardPlans

Your Saturday in Porto: keep or skip?

A day timeline with a Keep/Skip toggle per stop; the times, end time and walking re-calculate live. Good for any itinerary the owner should trim before you book.

Owner sees
Six Porto stops on a timeline with walking times, start-time chips, a live “back by” summary and Lock in 5 stops.
Then
start/keep/skip: book the timed tickets, add the stops to the calendar with walking directions. replan: ask what kind of day they want instead.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Your Saturday in Porto: keep or skip?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "start": "09:30",
      "keep": [
        "livraria",
        "clerigos",
        "bolhao",
        "ribeira",
        "gaia"
      ],
      "skip": [
        "serralves"
      ],
      "ends_at": "16:54",
      "walk_minutes": 59
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 10.2 KB
{
  "kind": "html",
  "title": "Your Saturday in Porto: keep or skip?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#fbf6ef;color:#2b2420;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:13px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#a8432d}\nh1{font-size:24px;line-height:1.2;margin:4px 0 4px;letter-spacing:-.01em}\n.sub{color:#6b5d54;font-size:15px;margin:0 0 16px}\n.lbl{font-size:13px;font-weight:700;color:#6b5d54;margin:0 0 8px}\n.chips{display:flex;gap:8px;margin-bottom:14px}\n.chip{flex:1;font:inherit;font-weight:700;font-variant-numeric:tabular-nums;min-height:48px;border-radius:999px;border:1.5px solid #e4d6c6;background:#fff;color:#2b2420;cursor:pointer;transition:all .18s}\n.chip.on{background:#c8553d;border-color:#c8553d;color:#fff;box-shadow:0 4px 12px rgba(200,85,61,.28)}\n.sum{display:grid;grid-template-columns:repeat(3,1fr);background:#fff;border-radius:16px;box-shadow:0 2px 10px rgba(43,36,32,.07);margin-bottom:18px}\n.sum div{padding:12px 6px;text-align:center}\n.sum div+div{border-left:1px solid #f0e6da}\n.sum b{display:block;font-size:20px;font-variant-numeric:tabular-nums;color:#1f6f6f;transition:color .2s}\n.sum span{font-size:12px;color:#6b5d54}\n.stop{display:grid;grid-template-columns:46px 1fr;gap:8px;position:relative}\n.time{font-weight:700;font-size:15px;font-variant-numeric:tabular-nums;padding-top:14px;text-align:right;transition:opacity .2s;line-height:1.2}\n.time small{display:block;font-weight:500;font-size:12px;color:#6b5d54}\n.card{display:flex;align-items:center;gap:12px;background:#fff;border-radius:16px;padding:12px;box-shadow:0 2px 10px rgba(43,36,32,.07);transition:opacity .2s,transform .2s;border-left:4px solid #c8553d}\n.ico{flex:none;width:40px;height:40px;border-radius:12px;background:#f8e7e1;display:grid;place-content:center;font-size:21px}\n.txt{flex:1;min-width:0}\n.txt b{display:block;font-size:16px;line-height:1.25}\n.txt small{color:#6b5d54;font-size:13px}\n.tg{flex:none;font:inherit;font-size:14px;font-weight:700;min-width:68px;min-height:48px;border-radius:12px;border:1.5px solid #1f6f6f;background:#e5f1f0;color:#1f6f6f;cursor:pointer;transition:all .18s}\n.stop.off .card{background:#f6efe6;box-shadow:none;border-left-color:#d8ccc0}\n.stop.off .ico,.stop.off .txt{opacity:.45}\n.stop.off .time{opacity:.35;text-decoration:line-through}\n.stop.off .tg{background:#fff;border-color:#cdbfb2;color:#5a4d44}\n.walk{display:grid;grid-template-columns:46px 1fr;gap:8px;height:34px}\n.walk i{display:block;border-left:2px dashed #d9c7b6;margin-left:18px;height:100%}\n.walk span{font-size:13px;color:#1f6f6f;font-weight:600;align-self:center;font-variant-numeric:tabular-nums}\n.walk.off span{color:#a89a8e}\n.foot{margin-top:20px}\nbutton.main{width:100%;font:inherit;font-weight:700;font-size:17px;border:0;border-radius:16px;padding:16px;min-height:56px;background:#c8553d;color:#fff;cursor:pointer;box-shadow:0 6px 16px rgba(200,85,61,.3);transition:transform .15s,opacity .2s}\nbutton.alt{width:100%;font:inherit;font-weight:600;border:0;border-radius:14px;padding:14px;min-height:48px;background:transparent;color:#1f6f6f;cursor:pointer;margin-top:6px}\nbutton:active{transform:scale(.98)}\nbutton:disabled{opacity:.5;cursor:default;transform:none}\n.status{text-align:center;color:#6b5d54;font-size:14px;min-height:20px;margin-top:6px}\n.done{color:#1f6f6f}\n</style>\n<div class=\"wrap\">\n  <div class=\"eyebrow\" id=\"eyebrow\"></div>\n  <h1 id=\"title\"></h1>\n  <p class=\"sub\" id=\"sub\"></p>\n  <p class=\"lbl\" id=\"startLbl\"></p>\n  <div class=\"chips\" id=\"chips\"></div>\n  <div class=\"sum\"><div><b id=\"sEnd\"></b><span id=\"sEndL\"></span></div><div><b id=\"sWalk\"></b><span id=\"sWalkL\"></span></div><div><b id=\"sKm\"></b><span id=\"sKmL\"></span></div></div>\n  <div id=\"line\"></div>\n  <div class=\"foot\"><button class=\"main\" id=\"go\"></button><button class=\"alt\" id=\"alt\"></button><div class=\"status\" id=\"status\"></div></div>\n</div>\n<script>\n// The card's content. Change only this object.\n// answer.data: {start:\"09:30\", keep:[ids], skip:[ids], ends_at:\"18:40\", walk_minutes:57} or {action:\"replan\"}\nvar CARD = {\n  eyebrow: \"Porto · Sat 14 Nov\",\n  title: \"Your Saturday in Porto: keep or skip?\",\n  sub: \"I planned six stops. Skip any you don't fancy and the day re-times itself.\",\n  startLabel: \"Leave the hotel at\",\n  starts: [\"09:00\", \"09:30\", \"10:00\"],\n  start: \"09:30\",\n  stats: { end: \"back by\", walk: \"min walking\", km: \"km on foot\" },\n  walkWord: \"min walk\", keepWord: \"✓ Keep\", skipWord: \"Skipped\",\n  // x, y: rough map position in km, used to time the walk between kept stops\n  stops: [\n    { id: \"livraria\", icon: \"📚\", name: \"Livraria Lello\", note: \"Booked entry\", stay: 45, x: 0, y: 0, keep: true },\n    { id: \"clerigos\", icon: \"🗼\", name: \"Clérigos Tower\", note: \"Best city view\", stay: 40, x: 0.15, y: -0.1, keep: true },\n    { id: \"bolhao\", icon: \"🐟\", name: \"Mercado do Bolhão\", note: \"Lunch at the market\", stay: 90, x: 0.9, y: 0.5, keep: true },\n    { id: \"serralves\", icon: \"🌳\", name: \"Serralves Park\", note: \"Lovely, but far out west\", stay: 120, x: -4.0, y: 1.0, keep: false },\n    { id: \"ribeira\", icon: \"⛵\", name: \"Ribeira riverside\", note: \"Walk the Dom Luís bridge\", stay: 120, x: 0.4, y: -0.9, keep: true },\n    { id: \"gaia\", icon: \"🍷\", name: \"Graham's cellars\", note: \"Port tasting, 3 glasses\", stay: 90, x: -0.3, y: -1.6, keep: true }\n  ],\n  walkKmh: 4.8,\n  primary: \"Lock in {n} stops\",\n  primaryOne: \"Lock in 1 stop\",\n  emptyPrimary: \"Keep at least one stop\",\n  secondary: \"Plan a different day\",\n  sending: \"Sending…\",\n  doneTitle: \"Day locked in\",\n  doneSub: \"I'll put it in your calendar with walking directions.\",\n  preview: \"Preview only: open this card in Bridgit to answer.\"\n};\n\nvar sent = false, start = CARD.start;\nfunction $(id){ return document.getElementById(id); }\nfunction mins(t){ var p = t.split(\":\"); return +p[0] * 60 + +p[1]; }\nfunction hhmm(m){ var h = Math.floor(m / 60) % 24, x = m % 60; return (h < 10 ? \"0\" : \"\") + h + \":\" + (x < 10 ? \"0\" : \"\") + x; }\nfunction dur(m){ return m >= 60 && m % 60 === 0 ? m / 60 + \"\\u00a0h\" : m + \"\\u00a0min\"; }\nfunction el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }\n\n$(\"eyebrow\").textContent = CARD.eyebrow; $(\"title\").textContent = CARD.title; $(\"sub\").textContent = CARD.sub;\n$(\"startLbl\").textContent = CARD.startLabel; $(\"alt\").textContent = CARD.secondary;\n$(\"sEndL\").textContent = CARD.stats.end; $(\"sWalkL\").textContent = CARD.stats.walk; $(\"sKmL\").textContent = CARD.stats.km;\n\nCARD.starts.forEach(function(t){\n  var b = el(\"button\", \"chip\", t);\n  b.onclick = function(){ if (sent) return; start = t; render(); };\n  b.dataset.t = t; $(\"chips\").appendChild(b);\n});\n\nvar rows = CARD.stops.map(function(s, i){\n  var w = null;\n  if (i > 0) { w = el(\"div\", \"walk\"); w.appendChild(el(\"i\")); w.appendChild(el(\"span\")); $(\"line\").appendChild(w); }\n  var r = el(\"div\", \"stop\"), t = el(\"div\", \"time\"), c = el(\"div\", \"card\"), tx = el(\"div\", \"txt\"), tg = el(\"button\", \"tg\");\n  c.appendChild(el(\"div\", \"ico\", s.icon));\n  tx.appendChild(el(\"b\", null, s.name)); tx.appendChild(el(\"small\", null, s.note)); c.appendChild(tx);\n  tg.onclick = function(){ if (sent) return; s.keep = !s.keep; render(); };\n  c.appendChild(tg); r.appendChild(t); r.appendChild(c); $(\"line\").appendChild(r);\n  return { s: s, row: r, time: t, tg: tg, walk: w };\n});\n\nfunction render(){\n  document.querySelectorAll(\".chip\").forEach(function(b){ b.classList.toggle(\"on\", b.dataset.t === start); });\n  var now = mins(start), walk = 0, km = 0, prev = null, n = 0;\n  rows.forEach(function(r){\n    var s = r.s;\n    if (r.walk) r.walk.classList.toggle(\"off\", !s.keep);\n    if (s.keep) {\n      if (prev) {\n        var d = Math.hypot(s.x - prev.x, s.y - prev.y) * 1.3, m = Math.round(d / CARD.walkKmh * 60);\n        now += m; walk += m; km += d;\n        if (r.walk) r.walk.lastChild.textContent = \"🚶 \" + m + \" \" + CARD.walkWord;\n      } else if (r.walk) r.walk.lastChild.textContent = \"\";\n      r.time.textContent = hhmm(now); r.time.appendChild(el(\"small\", null, dur(s.stay))); now += s.stay; prev = s; n++;\n    } else {\n      if (r.walk) r.walk.lastChild.textContent = \"\";\n      r.time.textContent = \"\";\n    }\n    r.row.classList.toggle(\"off\", !s.keep);\n    r.tg.textContent = s.keep ? CARD.keepWord : CARD.skipWord;\n    r.tg.setAttribute(\"aria-pressed\", s.keep ? \"true\" : \"false\");\n  });\n  $(\"sEnd\").textContent = n ? hhmm(now) : \"–\";\n  $(\"sWalk\").textContent = walk; $(\"sKm\").textContent = km.toFixed(1);\n  var go = $(\"go\");\n  go.disabled = !n || sent;\n  go.textContent = !n ? CARD.emptyPrimary : n === 1 ? CARD.primaryOne : CARD.primary.replace(\"{n}\", n);\n  return { ends: hhmm(now), walk: walk };\n}\n\nfunction send(answer){\n  if (sent) return;\n  var B = window.Bridgit || window.Bridget;\n  if (!B || !B.respond) { $(\"status\").textContent = CARD.preview; return; }\n  sent = true;\n  document.querySelectorAll(\"button\").forEach(function(b){ b.disabled = true; });\n  $(\"status\").textContent = CARD.sending;\n  B.respond(JSON.stringify(answer));\n}\n$(\"go\").onclick = function(){\n  var r = render();\n  send({ start: start,\n    keep: CARD.stops.filter(function(s){ return s.keep; }).map(function(s){ return s.id; }),\n    skip: CARD.stops.filter(function(s){ return !s.keep; }).map(function(s){ return s.id; }),\n    ends_at: r.ends, walk_minutes: r.walk });\n};\n$(\"alt\").onclick = function(){ send({ action: \"replan\" }); };\ndocument.addEventListener(\"bridgit:answered\", function(e){\n  e.preventDefault();\n  $(\"title\").textContent = \"✓ \" + CARD.doneTitle; $(\"title\").className = \"done\";\n  $(\"sub\").textContent = CARD.doneSub; $(\"status\").textContent = \"\";\n  setTimeout(function(){ $(\"title\").scrollIntoView({ block: \"center\" }); }, 400);\n});\nrender();\n</script>\n",
  "expires_in_seconds": 43200
}
Raw JSONhtmltimelinetravel
HTML cardPlans

Team dinner: which evening works?

A month calendar tinted by how many people are free each evening, with time chips and who can't make it. Good for any group date where the owner makes the final call.

Owner sees
December 2026 with candidate evenings shaded by availability, four start times, the names that miss out, and Propose Thu 10 Dec, 19:30.
Then
date/time: send the calendar invite to the team and book a table for the free count. none_work: poll the team for January dates.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Team dinner: which evening works?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "date": "2026-12-10",
      "time": "19:30",
      "free": 7,
      "missing": [
        "Ola"
      ]
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 10.7 KB
{
  "kind": "html",
  "title": "Team dinner: which evening works?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:linear-gradient(180deg,#f6f5ff 0%,#ffffff 60%);color:#1e1b3a;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:700;color:#4646b8;background:#ebebfd;border-radius:999px;padding:5px 10px}\nh1{font-size:24px;line-height:1.2;margin:10px 0 4px;letter-spacing:-.01em}\n.sub{color:#5d5a7a;font-size:15px;margin:0 0 16px}\n.cal{background:#fff;border-radius:20px;padding:14px 12px 12px;box-shadow:0 4px 18px rgba(60,55,140,.09)}\n.mon{display:flex;justify-content:space-between;align-items:baseline;margin:0 4px 10px}\n.mon b{font-size:17px}\n.mon span{font-size:12px;color:#5d5a7a}\n.grid{display:grid;grid-template-columns:repeat(7,1fr);gap:5px}\n.dow{text-align:center;font-size:12px;font-weight:700;color:#7a77a0;padding-bottom:2px}\n.day{font:inherit;border:0;border-radius:12px;min-height:50px;padding:4px 0;background:transparent;color:#bdbbd0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;font-variant-numeric:tabular-nums;transition:transform .15s,box-shadow .18s}\n.day b{font-size:15px;font-weight:600;line-height:1.1}\n.day small{font-size:11px;font-weight:700;line-height:1}\n.day.c{cursor:pointer}\n.day.on{box-shadow:0 0 0 2.5px #fff,0 0 0 5px #2f2d8c;transform:scale(1.04)}\n.day.c:active{transform:scale(.96)}\n.key{display:flex;align-items:center;gap:8px;justify-content:flex-end;font-size:12px;color:#5d5a7a;margin:10px 4px 0}\n.key i{display:block;width:90px;height:8px;border-radius:4px;background:linear-gradient(90deg,#efeefd,#403ec4)}\n.lbl{font-size:13px;font-weight:700;color:#5d5a7a;margin:18px 0 8px}\n.times{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}\n.tm{font:inherit;min-height:56px;border-radius:14px;border:1.5px solid #dcdaf5;background:#fff;color:#1e1b3a;cursor:pointer;font-variant-numeric:tabular-nums;transition:all .18s}\n.tm b{display:block;font-size:16px}\n.tm small{font-size:12px;color:#5d5a7a}\n.tm.on{background:#5b5bd6;border-color:#5b5bd6;color:#fff;box-shadow:0 6px 14px rgba(91,91,214,.3)}\n.tm.on small{color:#e4e4ff}\n.who{background:#fff;border-radius:16px;padding:12px 14px;margin-top:12px;box-shadow:0 2px 10px rgba(60,55,140,.06);min-height:58px}\n.who p{margin:0 0 8px;font-size:14px;color:#5d5a7a}\n.ppl{display:flex;flex-wrap:wrap;gap:8px}\n.pp{display:flex;align-items:center;gap:6px;background:#f4f3ff;border-radius:999px;padding:4px 10px 4px 4px;font-size:14px;font-weight:600}\n.pp i{width:24px;height:24px;border-radius:50%;background:#c9c8f3;color:#2f2d8c;display:grid;place-content:center;font-style:normal;font-size:12px;font-weight:800}\n.all{font-weight:700;color:#2f8a5a}\n.foot{margin-top:20px}\nbutton.main{width:100%;font:inherit;font-weight:700;font-size:17px;border:0;border-radius:16px;padding:16px;min-height:56px;background:#5b5bd6;color:#fff;cursor:pointer;box-shadow:0 8px 18px rgba(91,91,214,.32);transition:transform .15s,opacity .2s}\nbutton.alt{width:100%;font:inherit;font-weight:600;border:0;border-radius:14px;padding:14px;min-height:48px;background:transparent;color:#4646b8;cursor:pointer;margin-top:6px}\nbutton:active{transform:scale(.98)}\nbutton:disabled{opacity:.5;cursor:default;transform:none}\n.status{text-align:center;color:#5d5a7a;font-size:14px;min-height:20px;margin-top:6px}\n.done{color:#4646b8}\n</style>\n<div class=\"wrap\">\n  <span class=\"eyebrow\" id=\"eyebrow\"></span>\n  <h1 id=\"title\"></h1>\n  <p class=\"sub\" id=\"sub\"></p>\n  <div class=\"cal\">\n    <div class=\"mon\"><b id=\"month\"></b><span id=\"hint\"></span></div>\n    <div class=\"grid\" id=\"grid\"></div>\n    <div class=\"key\"><span id=\"kLo\"></span><i></i><span id=\"kHi\"></span></div>\n  </div>\n  <p class=\"lbl\" id=\"timeLbl\"></p>\n  <div class=\"times\" id=\"times\"></div>\n  <div class=\"who\"><p id=\"whoLbl\"></p><div class=\"ppl\" id=\"ppl\"></div></div>\n  <div class=\"foot\"><button class=\"main\" id=\"go\"></button><button class=\"alt\" id=\"alt\"></button><div class=\"status\" id=\"status\"></div></div>\n</div>\n<script>\n// The card's content. Change only this object.\n// answer.data: {date:\"2026-12-10\", time:\"19:30\", free:7, missing:[\"Ola\"]} or {action:\"none_work\"}\nvar CARD = {\n  eyebrow: \"👥 Design team · 8 people\",\n  title: \"Team dinner: which evening works?\",\n  sub: \"I checked everyone's calendars. Darker evenings have more people free.\",\n  year: 2026, month: 12, monthName: \"December 2026\",\n  hint: \"Tap an evening\",\n  weekdays: [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n  dayNames: [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"],\n  shortMonth: \"Dec\",\n  keyLow: \"fewer\", keyHigh: \"more free\",\n  people: [\n    { name: \"Ania\" }, { name: \"Bartek\", earliest: \"19:00\" }, { name: \"Kasia\" }, { name: \"Marek\" },\n    { name: \"Ola\" }, { name: \"Piotr\" }, { name: \"Zosia\", earliest: \"19:30\" }, { name: \"Tomek\", latest: \"19:30\" }\n  ],\n  // candidate evenings: day of month -> who is busy all evening\n  days: {\n    1: [\"Ania\", \"Piotr\", \"Tomek\"], 2: [\"Marek\", \"Kasia\"], 3: [\"Ola\", \"Ania\", \"Piotr\", \"Kasia\"],\n    4: [\"Tomek\", \"Marek\"], 8: [\"Kasia\", \"Zosia\", \"Ola\", \"Bartek\", \"Piotr\"], 9: [\"Piotr\", \"Ania\"],\n    10: [\"Ola\"], 11: [\"Marek\", \"Tomek\", \"Ania\"], 15: [\"Bartek\", \"Kasia\"],\n    16: [\"Ania\", \"Marek\", \"Ola\", \"Tomek\", \"Kasia\", \"Piotr\"], 17: [\"Zosia\", \"Ola\"]\n  },\n  times: [\"18:30\", \"19:00\", \"19:30\", \"20:00\"],\n  pick: { day: 10, time: \"19:30\" },\n  timeLabel: \"Start time\",\n  whoLabel: \"Can't make it\",\n  everyone: \"Everyone's in 🎉\",\n  primary: \"Propose {day}, {time}\",\n  secondary: \"None of these work\",\n  sending: \"Sending…\",\n  doneTitle: \"Proposed to the team\",\n  doneSub: \"I'll send the invite and book a table for {n}.\",\n  preview: \"Preview only: open this card in Bridgit to answer.\"\n};\n\nvar sent = false, day = CARD.pick.day, time = CARD.pick.time, cells = {}, chips = [];\nfunction $(id){ return document.getElementById(id); }\nfunction el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }\nfunction m(t){ var p = t.split(\":\"); return +p[0] * 60 + +p[1]; }\nfunction missing(d, t){\n  var busy = CARD.days[d] || [];\n  return CARD.people.filter(function(p){\n    return busy.indexOf(p.name) >= 0 || (p.earliest && m(t) < m(p.earliest)) || (p.latest && m(t) > m(p.latest));\n  }).map(function(p){ return p.name; });\n}\nfunction best(d){ return Math.max.apply(null, CARD.times.map(function(t){ return CARD.people.length - missing(d, t).length; })); }\nfunction tint(n){\n  var a = [239, 238, 253], b = [64, 62, 196], k = Math.max(0, (n - 1) / (CARD.people.length - 1));\n  return a.map(function(v, i){ return Math.round(v + (b[i] - v) * k); });\n}\nfunction label(d){ var w = new Date(CARD.year, CARD.month - 1, d).getDay(); return CARD.dayNames[w] + \" \" + d + \" \" + CARD.shortMonth; }\n\n[\"eyebrow\", \"title\", \"sub\"].forEach(function(k){ $(k).textContent = CARD[k]; });\n$(\"month\").textContent = CARD.monthName; $(\"hint\").textContent = CARD.hint;\n$(\"kLo\").textContent = CARD.keyLow; $(\"kHi\").textContent = CARD.keyHigh;\n$(\"timeLbl\").textContent = CARD.timeLabel; $(\"whoLbl\").textContent = CARD.whoLabel; $(\"alt\").textContent = CARD.secondary;\n\nCARD.weekdays.forEach(function(w){ $(\"grid\").appendChild(el(\"div\", \"dow\", w)); });\nvar lead = (new Date(CARD.year, CARD.month - 1, 1).getDay() + 6) % 7, total = new Date(CARD.year, CARD.month, 0).getDate();\nfor (var i = 0; i < lead; i++) $(\"grid\").appendChild(el(\"div\"));\nfor (var d = 1; d <= total; d++) (function(d){\n  var cand = !!CARD.days[d], c = el(cand ? \"button\" : \"div\", \"day\" + (cand ? \" c\" : \"\"));\n  c.appendChild(el(\"b\", null, String(d)));\n  if (cand) {\n    var n = best(d);\n    c.appendChild(el(\"small\", null, n + \"/\" + CARD.people.length));\n    var rgb = tint(n), L = rgb.map(function(v){ v /= 255; return v <= .03928 ? v / 12.92 : Math.pow((v + .055) / 1.055, 2.4); });\n    L = .2126 * L[0] + .7152 * L[1] + .0722 * L[2];\n    c.style.background = \"rgb(\" + rgb.join(\",\") + \")\"; c.style.color = 1.05 / (L + .05) > (L + .05) / .0644 ? \"#fff\" : \"#1c1a4a\";\n    c.setAttribute(\"aria-label\", label(d) + \": \" + n + \" free\");\n    c.onclick = function(){ if (sent) return; day = d; render(); };\n    cells[d] = c;\n  }\n  $(\"grid\").appendChild(c);\n})(d);\n\nCARD.times.forEach(function(t){\n  var b = el(\"button\", \"tm\"); b.appendChild(el(\"b\", null, t)); b.appendChild(el(\"small\"));\n  b.onclick = function(){ if (sent) return; time = t; render(); };\n  chips.push({ t: t, b: b }); $(\"times\").appendChild(b);\n});\n\nfunction render(){\n  Object.keys(cells).forEach(function(k){ cells[k].classList.toggle(\"on\", +k === day); });\n  chips.forEach(function(c){\n    c.b.classList.toggle(\"on\", c.t === time);\n    c.b.lastChild.textContent = (CARD.people.length - missing(day, c.t).length) + \" free\";\n  });\n  var miss = missing(day, time), box = $(\"ppl\");\n  box.textContent = \"\";\n  if (!miss.length) box.appendChild(el(\"span\", \"all\", CARD.everyone));\n  miss.forEach(function(n){ var p = el(\"span\", \"pp\"); p.appendChild(el(\"i\", null, n.charAt(0))); p.appendChild(document.createTextNode(n)); box.appendChild(p); });\n  $(\"go\").textContent = CARD.primary.replace(\"{day}\", label(day)).replace(\"{time}\", time);\n  return miss;\n}\n\nfunction send(answer){\n  if (sent) return;\n  var B = window.Bridgit || window.Bridget;\n  if (!B || !B.respond) { $(\"status\").textContent = CARD.preview; return; }\n  sent = true;\n  document.querySelectorAll(\"button\").forEach(function(b){ b.disabled = true; });\n  $(\"status\").textContent = CARD.sending;\n  B.respond(JSON.stringify(answer));\n}\n$(\"go\").onclick = function(){\n  var miss = render(), mm = CARD.month < 10 ? \"0\" + CARD.month : CARD.month, dd = day < 10 ? \"0\" + day : day;\n  send({ date: CARD.year + \"-\" + mm + \"-\" + dd, time: time, free: CARD.people.length - miss.length, missing: miss });\n};\n$(\"alt\").onclick = function(){ send({ action: \"none_work\" }); };\ndocument.addEventListener(\"bridgit:answered\", function(e){\n  e.preventDefault();\n  $(\"title\").textContent = \"✓ \" + CARD.doneTitle; $(\"title\").className = \"done\";\n  $(\"sub\").textContent = CARD.doneSub.replace(\"{n}\", CARD.people.length - missing(day, time).length);\n  $(\"status\").textContent = \"\";\n  setTimeout(function(){ $(\"title\").scrollIntoView({ block: \"center\" }); }, 400);\n});\nrender();\n</script>\n",
  "expires_in_seconds": 172800
}
Raw JSONhtmlcalendargroup
HTML cardPlans

Kraków → Lisbon: pick a flight

Three flight cards side by side with times, route line, stops, price and CO₂, the agent's pick highlighted, and a bag toggle that updates the price. Good for any compare-and-buy choice.

Owner sees
Ryanair (best balance, my pick), Wizz Air (cheapest) and TAP (fastest), a checked-bag switch and Book Ryanair · €148.
Then
flight: book it (with the bag if asked) and send the confirmation. more_options: search again with later or cheaper flights.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Kraków → Lisbon: pick a flight",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "flight": "FR 2417",
      "airline": "Ryanair",
      "bag": true,
      "price_eur": 183,
      "agent_pick": true
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 10.9 KB
{
  "kind": "html",
  "title": "Kraków → Lisbon: pick a flight",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:linear-gradient(180deg,#e3f0ff 0%,#f5f9ff 45%,#ffffff 100%);color:#0b2545;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:13px;font-weight:700;color:#0060c7;letter-spacing:.02em}\nh1{font-size:24px;line-height:1.2;margin:4px 0 4px;letter-spacing:-.01em}\n.sub{color:#4a5d78;font-size:15px;margin:0 0 16px}\n.opt{display:block;width:100%;text-align:left;font:inherit;color:inherit;background:#fff;border:2px solid transparent;border-radius:18px;padding:14px 14px 12px;margin-bottom:12px;box-shadow:0 4px 16px rgba(11,37,69,.08);cursor:pointer;position:relative;transition:border-color .18s,box-shadow .18s,transform .15s}\n.opt:active{transform:scale(.98)}\n.opt.on{border-color:#0a84ff;box-shadow:0 8px 22px rgba(10,132,255,.22)}\n.hd{display:flex;align-items:center;gap:10px}\n.av{flex:none;width:38px;height:38px;border-radius:50%;display:grid;place-content:center;color:#fff;font-weight:800;font-size:17px}\n.al{flex:1;min-width:0}\n.al b{display:block;font-size:15px}\n.al small{color:#4a5d78;font-size:13px}\n.pr{text-align:right;font-variant-numeric:tabular-nums}\n.pr b{display:block;font-size:21px;letter-spacing:-.01em}\n.pr small{color:#4a5d78;font-size:12px}\n.rt{display:flex;align-items:center;gap:10px;margin:12px 0 10px}\n.end{font-variant-numeric:tabular-nums}\n.end b{display:block;font-size:19px;line-height:1.1}\n.end small{color:#4a5d78;font-size:12px;font-weight:600}\n.end.r{text-align:right}\n.mid{flex:1;text-align:center;font-size:12px;color:#4a5d78}\n.ln{position:relative;height:12px;margin:3px 0;display:flex;align-items:center;justify-content:space-evenly}\n.ln:before{content:\"\";position:absolute;left:0;right:0;top:50%;border-top:1.5px solid #b9cbe3}\n.ln i{position:relative;width:9px;height:9px;border-radius:50%;background:#fff;border:2px solid #0a84ff}\n.ln:after{content:\"\";position:absolute;right:-2px;top:50%;width:6px;height:6px;border-radius:50%;background:#0b2545;transform:translateY(-50%)}\n.ln span{position:absolute;left:-2px;top:50%;width:6px;height:6px;border-radius:50%;background:#0b2545;transform:translateY(-50%)}\n.ft{display:flex;align-items:center;justify-content:space-between;gap:8px;border-top:1px solid #edf2f9;padding-top:10px}\n.bd{font-size:12px;font-weight:700;border-radius:999px;padding:4px 10px;background:#eef3fa;color:#34506f}\n.bd.pick{background:#0062cc;color:#fff}\n.co{font-size:12px;color:#2e6b3f;font-weight:600;font-variant-numeric:tabular-nums}\n.ck{position:absolute;top:-8px;right:-8px;width:26px;height:26px;border-radius:50%;background:#0a84ff;color:#fff;display:grid;place-content:center;font-size:14px;font-weight:800;opacity:0;transform:scale(.6);transition:all .18s;box-shadow:0 2px 6px rgba(10,132,255,.4)}\n.opt.on .ck{opacity:1;transform:none}\n.bag{display:flex;align-items:center;gap:12px;width:100%;font:inherit;color:inherit;text-align:left;background:#fff;border:0;border-radius:16px;padding:12px 14px;min-height:60px;box-shadow:0 2px 10px rgba(11,37,69,.06);cursor:pointer;margin-top:4px}\n.bag svg{flex:none}\n.bag span{flex:1;font-weight:600}\n.bag small{display:block;font-weight:400;color:#4a5d78;font-size:13px}\n.sw{flex:none;width:48px;height:28px;border-radius:14px;background:#c9d5e6;position:relative;transition:background .18s}\n.sw:after{content:\"\";position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);transition:transform .18s}\n.bag.on .sw{background:#0a72e0}\n.bag.on .sw:after{transform:translateX(20px)}\n.bag.inc .sw{display:none}\n.foot{margin-top:20px}\nbutton.main{width:100%;font:inherit;font-weight:700;font-size:17px;border:0;border-radius:16px;padding:16px;min-height:56px;background:#0a72e0;color:#fff;cursor:pointer;box-shadow:0 8px 20px rgba(10,132,255,.32);font-variant-numeric:tabular-nums;transition:transform .15s,opacity .2s}\nbutton.alt{width:100%;font:inherit;font-weight:600;border:0;border-radius:14px;padding:14px;min-height:48px;background:transparent;color:#0060c7;cursor:pointer;margin-top:6px}\nbutton.main:active,button.alt:active,.bag:active{transform:scale(.98)}\nbutton:disabled{opacity:.5;cursor:default;transform:none}\n.status{text-align:center;color:#4a5d78;font-size:14px;min-height:20px;margin-top:6px}\n.done{color:#0060c7}\n</style>\n<div class=\"wrap\">\n  <div class=\"eyebrow\" id=\"eyebrow\"></div>\n  <h1 id=\"title\"></h1>\n  <p class=\"sub\" id=\"sub\"></p>\n  <div id=\"list\"></div>\n  <button class=\"bag\" id=\"bag\"><svg width=\"34\" height=\"34\" viewBox=\"0 0 34 34\" aria-hidden=\"true\"><rect width=\"34\" height=\"34\" rx=\"10\" fill=\"#e3f0ff\"/><rect x=\"10\" y=\"11\" width=\"14\" height=\"15\" rx=\"3\" fill=\"none\" stroke=\"#0a72e0\" stroke-width=\"2\"/><path d=\"M14 11V8.5h6V11M14 15v7M20 15v7\" stroke=\"#0a72e0\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/></svg><span id=\"bagTxt\"></span><i class=\"sw\"></i></button>\n  <div class=\"foot\"><button class=\"main\" id=\"go\"></button><button class=\"alt\" id=\"alt\"></button><div class=\"status\" id=\"status\"></div></div>\n</div>\n<script>\n// The card's content. Change only this object.\n// answer.data: {flight:\"FR 2417\", airline:\"Ryanair\", bag:true, price_eur:183, agent_pick:true} or {action:\"more_options\"}\nvar CARD = {\n  eyebrow: \"Fri 4 Dec · 1 adult · cabin bag included\",\n  title: \"Kraków → Lisbon: pick a flight\",\n  sub: \"Three good options. I'd take the Ryanair: direct, mid-morning and fair.\",\n  pick: \"FR 2417\",\n  flights: [\n    { id: \"FR 2417\", airline: \"Ryanair\", color: \"#073590\", dep: \"11:25\", arr: \"14:20\", from: \"KRK\", to: \"LIS\", dur: \"3 h 55 min\", stops: [], price: 148, bag: 35, co2: 175, badge: \"Best balance\" },\n    { id: \"W6 1305\", airline: \"Wizz Air\", color: \"#c6007e\", dep: \"06:10\", arr: \"12:40\", from: \"KRK\", to: \"LIS\", dur: \"7 h 30 min\", stops: [\"FCO\"], price: 92, bag: 38, co2: 214, badge: \"Cheapest\" },\n    { id: \"TP 1281\", airline: \"TAP Air Portugal\", color: \"#12784a\", dep: \"07:00\", arr: \"09:45\", from: \"KRK\", to: \"LIS\", dur: \"3 h 45 min\", stops: [], price: 239, bag: 0, co2: 189, badge: \"Fastest\" }\n  ],\n  direct: \"Direct\", oneStop: \"1 stop · \", stopsWord: \" stops · \",\n  pickNote: \" · my pick\",\n  perPerson: \"one way\",\n  co2: \"kg CO₂\",\n  bagLabel: \"Add a checked bag\",\n  bagSub: \"23 kg, dropped at the desk\",\n  bagIncluded: \"Checked bag included\",\n  primary: \"Book {airline} · €{price}\",\n  secondary: \"Show me more flights\",\n  sending: \"Sending…\",\n  doneTitle: \"Booking your flight\",\n  doneSub: \"I'll send the boarding pass when check-in opens.\",\n  preview: \"Preview only: open this card in Bridgit to answer.\"\n};\n\nvar sent = false, sel = CARD.pick, bag = false, cards = [];\nfunction $(id){ return document.getElementById(id); }\nfunction el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }\nfunction cur(){ return CARD.flights.filter(function(f){ return f.id === sel; })[0]; }\n\n[\"eyebrow\", \"title\", \"sub\"].forEach(function(k){ $(k).textContent = CARD[k]; });\n$(\"alt\").textContent = CARD.secondary;\n\nCARD.flights.forEach(function(f){\n  var b = el(\"button\", \"opt\"), hd = el(\"div\", \"hd\"), av = el(\"div\", \"av\", f.airline.charAt(0)), al = el(\"div\", \"al\"), pr = el(\"div\", \"pr\");\n  av.style.background = f.color;\n  al.appendChild(el(\"b\", null, f.airline)); al.appendChild(el(\"small\", null, f.id));\n  pr.appendChild(el(\"b\", null, \"€\" + f.price)); pr.appendChild(el(\"small\", null, CARD.perPerson));\n  hd.appendChild(av); hd.appendChild(al); hd.appendChild(pr); b.appendChild(hd);\n  var rt = el(\"div\", \"rt\"), a = el(\"div\", \"end\"), z = el(\"div\", \"end r\"), mid = el(\"div\", \"mid\"), ln = el(\"div\", \"ln\");\n  a.appendChild(el(\"b\", null, f.dep)); a.appendChild(el(\"small\", null, f.from));\n  z.appendChild(el(\"b\", null, f.arr)); z.appendChild(el(\"small\", null, f.to));\n  ln.appendChild(el(\"span\")); f.stops.forEach(function(){ ln.appendChild(el(\"i\")); });\n  mid.appendChild(el(\"div\", null, f.dur)); mid.appendChild(ln);\n  mid.appendChild(el(\"div\", null, !f.stops.length ? CARD.direct : f.stops.length === 1 ? CARD.oneStop + f.stops[0] : f.stops.length + CARD.stopsWord + f.stops.join(\", \")));\n  rt.appendChild(a); rt.appendChild(mid); rt.appendChild(z); b.appendChild(rt);\n  var ft = el(\"div\", \"ft\"), isPick = f.id === CARD.pick;\n  ft.appendChild(el(\"span\", \"bd\" + (isPick ? \" pick\" : \"\"), (isPick ? \"★ \" : \"\") + f.badge + (isPick ? CARD.pickNote : \"\")));\n  ft.appendChild(el(\"span\", \"co\", \"🌱 \" + f.co2 + \" \" + CARD.co2)); b.appendChild(ft);\n  b.appendChild(el(\"span\", \"ck\", \"✓\"));\n  b.onclick = function(){ if (sent) return; sel = f.id; render(); };\n  cards.push({ f: f, b: b }); $(\"list\").appendChild(b);\n});\n\n$(\"bag\").onclick = function(){ if (sent || !cur().bag) return; bag = !bag; render(); };\n\nfunction price(){ var f = cur(); return f.price + (bag && f.bag ? f.bag : 0); }\nfunction render(){\n  var f = cur();\n  cards.forEach(function(c){ c.b.classList.toggle(\"on\", c.f.id === sel); c.b.setAttribute(\"aria-pressed\", c.f.id === sel ? \"true\" : \"false\"); });\n  var t = $(\"bagTxt\"), bb = $(\"bag\");\n  t.textContent = \"\";\n  if (f.bag) { t.appendChild(document.createTextNode(CARD.bagLabel + \" (+€\" + f.bag + \")\")); t.appendChild(el(\"small\", null, CARD.bagSub)); }\n  else t.appendChild(document.createTextNode(\"✓ \" + CARD.bagIncluded));\n  bb.classList.toggle(\"on\", bag && !!f.bag); bb.classList.toggle(\"inc\", !f.bag);\n  bb.setAttribute(\"aria-pressed\", bag && f.bag ? \"true\" : \"false\");\n  $(\"go\").textContent = CARD.primary.replace(\"{airline}\", f.airline).replace(\"{price}\", price());\n}\n\nfunction send(answer){\n  if (sent) return;\n  var B = window.Bridgit || window.Bridget;\n  if (!B || !B.respond) { $(\"status\").textContent = CARD.preview; return; }\n  sent = true;\n  document.querySelectorAll(\"button\").forEach(function(b){ b.disabled = true; });\n  $(\"status\").textContent = CARD.sending;\n  B.respond(JSON.stringify(answer));\n}\n$(\"go\").onclick = function(){\n  var f = cur();\n  send({ flight: f.id, airline: f.airline, bag: f.bag ? bag : true, price_eur: price(), agent_pick: f.id === CARD.pick });\n};\n$(\"alt\").onclick = function(){ send({ action: \"more_options\" }); };\ndocument.addEventListener(\"bridgit:answered\", function(e){\n  e.preventDefault();\n  $(\"title\").textContent = \"✓ \" + CARD.doneTitle; $(\"title\").className = \"done\";\n  $(\"sub\").textContent = CARD.doneSub; $(\"status\").textContent = \"\";\n  setTimeout(function(){ $(\"title\").scrollIntoView({ block: \"center\" }); }, 400);\n});\nrender();\n</script>\n",
  "expires_in_seconds": 21600
}
Raw JSONhtmlcomparetravel

Home

HTML cardHome

Saturday shop: what do you already have?

A checklist with sections, big tick boxes and an Add field. Ticked items are skipped.

Owner sees
The shopping list in aisles, tick boxes, a field to add more, and Order the rest.
Then
Fill the online basket with the order list plus anything added, then send the total as an approve-payment card.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Saturday shop: what do you already have?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "order": [
        "spinach",
        "lemons",
        "tomatoes",
        "avocado",
        "feta",
        "orzo",
        "chickpeas",
        "salmon"
      ],
      "already_have": [
        "garlic",
        "eggs",
        "yogurt",
        "olive_oil"
      ],
      "added": [
        "Coffee beans"
      ],
      "slot": "10:00-11:00"
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 5.1 KB
{
  "kind": "html",
  "title": "Saturday shop: what do you already have?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#f3f7ef;color:#1d2a1a;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:640px;margin:0 auto;padding:20px 16px 120px}\nh1{font-size:24px;margin:0 0 2px}\n.sub{color:#5f6f5a;margin:0 0 14px}\n.sec{margin:16px 0 6px;font-size:13px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#4f7a3a;display:flex;gap:6px;align-items:center}\nul{list-style:none;margin:0;padding:0;background:#fff;border-radius:14px;box-shadow:0 1px 0 #dbe4d3;overflow:hidden}\nli{border-top:1px solid #eef2ea}\nli:first-child{border-top:0}\nlabel{display:flex;align-items:center;gap:12px;padding:12px 14px;min-height:52px;cursor:pointer}\ninput[type=checkbox]{appearance:none;-webkit-appearance:none;width:26px;height:26px;border:2px solid #9bb38c;border-radius:8px;margin:0;flex:none;display:grid;place-content:center;background:#fff}\ninput[type=checkbox]:checked{background:#4f7a3a;border-color:#4f7a3a}\ninput[type=checkbox]:checked::after{content:\"\";width:7px;height:13px;border:solid #fff;border-width:0 3px 3px 0;transform:rotate(45deg) translate(-1px,-1px)}\n.t{flex:1}\n.t small{display:block;color:#7c8a77;font-size:13px}\nli.have .t{color:#9aa596;text-decoration:line-through}\n.qty{font-size:14px;color:#5f6f5a}\n.add{display:flex;gap:8px;margin-top:12px}\n.add input{flex:1;font:inherit;padding:12px 14px;border-radius:12px;border:1px solid #c9d6c0;background:#fff;min-width:0}\n.add button{font:inherit;font-weight:600;border:0;border-radius:12px;padding:0 16px;background:#dfe9d6;color:#2f4a22}\n.bar{position:fixed;left:0;right:0;bottom:0;background:#fbfdf9;border-top:1px solid #dbe4d3;padding:12px 16px 12px}\n.bar .in{max-width:640px;margin:0 auto;display:flex;gap:12px;align-items:center}\n.bar p{flex:1;margin:0;font-size:14px;color:#5f6f5a}\n.bar b{color:#1d2a1a}\n.go{font:inherit;font-weight:700;border:0;border-radius:12px;padding:14px 18px;min-height:48px;background:#4f7a3a;color:#fff}\n</style>\n<div class=\"wrap\">\n  <h1>Saturday shop</h1>\n  <p class=\"sub\">For the 3 dinners you planned. Tick what you <b>already have</b>; I'll order the rest for 10:00–11:00.</p>\n  <div id=\"list\"></div>\n  <div class=\"add\"><input id=\"addi\" maxlength=\"60\" placeholder=\"Add something else\" enterkeyhint=\"done\"><button id=\"addb\">Add</button></div>\n</div>\n<div class=\"bar\"><div class=\"in\"><p id=\"sum\"></p><button class=\"go\" id=\"go\">Order the rest</button></div></div>\n<script>\nvar data=[\n ['🥬 Produce',[['spinach','Baby spinach','200 g'],['lemons','Lemons','3'],['garlic','Garlic','1 bulb'],['tomatoes','Cherry tomatoes','250 g'],['avocado','Avocados','2, ripe']]],\n ['🧀 Dairy & eggs',[['eggs','Free-range eggs','6'],['feta','Feta','200 g'],['yogurt','Greek yogurt','500 g']]],\n ['🍝 Pantry',[['orzo','Orzo','500 g'],['chickpeas','Chickpeas','2 tins'],['olive_oil','Olive oil','only if low']]],\n ['🐟 Fish',[['salmon','Salmon fillets','2 × 140 g']]]\n],added=[],sent=false,list=document.getElementById('list');\nfunction render(){\n  list.innerHTML='';\n  data.forEach(function(sec){\n    var h=document.createElement('div');h.className='sec';h.textContent=sec[0];list.appendChild(h);\n    var ul=document.createElement('ul');\n    sec[1].forEach(function(it){\n      var li=document.createElement('li'),l=document.createElement('label'),c=document.createElement('input'),t=document.createElement('span'),q=document.createElement('span');\n      c.type='checkbox';c.value=it[0];c.onchange=function(){li.classList.toggle('have',c.checked);sum()};\n      t.className='t';t.textContent=it[1];q.className='qty';q.textContent=it[2];\n      l.append(c,t,q);li.appendChild(l);ul.appendChild(li);\n    });\n    list.appendChild(ul);\n  });\n  sum();\n}\nfunction sum(){\n  var all=document.querySelectorAll('input[type=checkbox]'),have=0;all.forEach(function(c){if(c.checked)have++});\n  document.getElementById('sum').innerHTML='<b>'+(all.length-have)+'</b> to order · '+have+' at home';\n}\nfunction addItem(){\n  var i=document.getElementById('addi'),v=i.value.trim();if(!v)return;\n  var id='extra_'+(added.length+1);added.push(v);\n  if(data[data.length-1][0]!=='➕ Added by you')data.push(['➕ Added by you',[]]);\n  data[data.length-1][1].push([id,v,'']);\n  var checked={};document.querySelectorAll('input:checked').forEach(function(c){checked[c.value]=1});\n  render();document.querySelectorAll('input[type=checkbox]').forEach(function(c){if(checked[c.value]){c.checked=true;c.closest('li').classList.add('have')}});sum();i.value='';\n}\ndocument.getElementById('addb').onclick=addItem;\ndocument.getElementById('addi').onkeydown=function(e){if(e.key==='Enter')addItem()};\ndocument.getElementById('go').onclick=function(){\n  if(sent)return;sent=true;var have=[],need=[];\n  document.querySelectorAll('input[type=checkbox]').forEach(function(c){if(c.value.indexOf('extra_')===0)return;(c.checked?have:need).push(c.value)});\n  (window.Bridgit||window.Bridget).respond(JSON.stringify({order:need,already_have:have,added:added,slot:'10:00-11:00'}));\n};\nrender();\n</script>\n"
}
Raw JSONhtmlcheckboxlist
Your agent
Evening check-in

Ten seconds. I keep these private and send you a weekly summary on Sundays.

How was today?
Sleep last night
QuestionHome

Evening check-in

Two choices and a free-text line. Same card each evening builds a journal.

Owner sees
Mood and sleep choices, an optional text field and Save.
Then
Store it. Once a week send a notice with the trend.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "Evening check-in",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "save",
    "values": {
      "mood": "🙂 Good",
      "sleep": "6-7 h",
      "note": "Long day, but the run helped"
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.9 KB
{
  "kind": "question",
  "title": "Evening check-in",
  "text": "Ten seconds. I keep these private and send you a weekly summary on Sundays.",
  "expires_in_seconds": 14400,
  "controls": [
    {
      "type": "choice",
      "id": "mood",
      "label": "How was today?",
      "options": [
        "😄 Great",
        "🙂 Good",
        "😐 Okay",
        "😕 Rough",
        "😫 Awful"
      ]
    },
    {
      "type": "choice",
      "id": "sleep",
      "label": "Sleep last night",
      "options": [
        "Under 5 h",
        "5-6 h",
        "6-7 h",
        "7-8 h",
        "Over 8 h"
      ]
    },
    {
      "type": "text",
      "id": "note",
      "label": "One line about today (optional)",
      "placeholder": "What stood out?"
    },
    {
      "type": "button",
      "id": "save",
      "label": "Save",
      "style": "primary"
    }
  ]
}
Raw JSONchoicetexthabit
Your agent
How was your Wolt delivery?

Thai Garden, delivered 19:42 (8 min early) by Kamil.

Rating
QuestionHome

How was your Wolt delivery?

Star rating as a choice, plus a comment. Works for any service.

Owner sees
A star choice, a comment field and Send rating / Skip.
Then
Submit the rating. Under 3 stars: offer to ask for a refund.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "How was your Wolt delivery?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "send",
    "values": {
      "stars": "★★★★☆",
      "comment": "Fast, but the bag was torn"
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.7 KB
{
  "kind": "question",
  "title": "How was your Wolt delivery?",
  "text": "Thai Garden, delivered 19:42 (8 min early) by Kamil.",
  "controls": [
    {
      "type": "choice",
      "id": "stars",
      "label": "Rating",
      "options": [
        "★★★★★",
        "★★★★☆",
        "★★★☆☆",
        "★★☆☆☆",
        "★☆☆☆☆"
      ]
    },
    {
      "type": "text",
      "id": "comment",
      "label": "Comment (optional)",
      "placeholder": "Anything to pass on?"
    },
    {
      "type": "button",
      "id": "send",
      "label": "Send rating",
      "style": "primary"
    },
    {
      "type": "button",
      "id": "skip",
      "label": "Skip"
    }
  ]
}
Raw JSONchoicetextrating
Your agentUrgent🔊 Read aloud
The courier is downstairs with your parcel

DPD, 1 parcel (the monitor arm). He can wait about 2 minutes.

QuestionHome

The courier is downstairs with your parcel

Urgent and spoken, three clear options. The courier is waiting.

Owner sees
An urgent card read aloud with three buttons.
Then
Relay the choice to the courier through the delivery app.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "The courier is downstairs with your parcel",
  "priority": "urgent",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "door",
    "values": {}
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.5 KB
{
  "kind": "question",
  "title": "The courier is downstairs with your parcel",
  "text": "DPD, 1 parcel (the monitor arm). He can wait about 2 minutes.",
  "priority": "urgent",
  "speak": true,
  "expires_in_seconds": 300,
  "controls": [
    {
      "type": "button",
      "id": "coming",
      "label": "I’m coming down",
      "style": "primary"
    },
    {
      "type": "button",
      "id": "door",
      "label": "Leave it at the door"
    },
    {
      "type": "button",
      "id": "neighbour",
      "label": "Leave with flat 3"
    }
  ]
}
Raw JSONurgentspeakbuttons
Your agent
Your parcel was delivered

Left at the front door at 11:42. The courier took a photo.

See the photo ↗Opens example.com
Dismiss
NoticeHome

Your parcel was delivered

A plain notice with a photo link. The default for good news.

Owner sees
A notice with a See the photo link.
Then
Nothing to wait for.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "notice",
  "status": "seen",
  "title": "Your parcel was delivered",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": null,
  "answered_at": null,
  "closed_at": null,
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.3 KB
{
  "kind": "notice",
  "title": "Your parcel was delivered",
  "text": "Left at the front door at 11:42. The courier took a photo.",
  "controls": [
    {
      "type": "link",
      "id": "photo",
      "label": "See the photo",
      "url": "https://example.com/track/DPD-0459-2291/photo"
    }
  ]
}
Raw JSONnoticelink
HTML cardHome

You've left home. Tidy up the house?

A smart-home control screen of device tiles that start on the agent's suggestions, so one tap applies them and any tile can be flipped first.

Owner sees
A tile grid: two lights on, the heating at 17° with − / + and an Away button, the front door to lock, the garage, and the washing machine finishing in 12 min.
Then
Switch off the listed lights, set the heating (null means leave it), lock the door, and warm the house back up when the owner is 20 minutes away. leave_as_is: change nothing.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "You've left home. Tidy up the house?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "lights_off": [
        "living",
        "kitchen"
      ],
      "lights_on": [],
      "heating_c": 17,
      "lock_door": true,
      "close_garage": false
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 13.1 KB
{
  "kind": "html",
  "title": "You've left home. Tidy up the house?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:linear-gradient(180deg,#f7f5f2 0%,#f3f1ee 40%,#ece8e2 100%);background-color:#f3f1ee;color:#24221f;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.head{display:flex;align-items:center;gap:14px;margin-bottom:18px}\n.house{width:60px;height:60px;border-radius:18px;background:#fff;box-shadow:0 6px 18px rgba(60,45,20,.08);display:grid;place-content:center;flex:none}\nh1{font-size:21px;line-height:1.3;margin:0 0 2px;letter-spacing:-.01em}\nh1.done{color:#2f8a4f}\n.since{font-size:14px;color:#6b655d;margin:0}\n.since:before{content:\"\";display:inline-block;width:8px;height:8px;border-radius:50%;background:#b8b1a6;margin-right:6px;vertical-align:1px}\n.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:18px}\n.tile{position:relative;font:inherit;text-align:left;color:inherit;background:rgba(255,255,255,.72);border:1px solid rgba(255,255,255,.9);border-radius:20px;padding:14px;min-height:118px;box-shadow:0 4px 16px rgba(60,45,20,.07);cursor:pointer;display:flex;flex-direction:column;justify-content:space-between;transition:background-color .2s,color .2s,box-shadow .2s,transform .15s}\n.tile:active,.main:active,.step:active,.away:active{transform:scale(.98)}\n.tile.wide{grid-column:1/-1;min-height:0;cursor:default}\n.ic{width:38px;height:38px;border-radius:12px;background:#ebe7e1;display:grid;place-content:center;transition:background-color .2s,color .2s;color:#6b655d}\n.ic svg{width:22px;height:22px}\n.tn{display:block;font-weight:600;font-size:15px;margin-top:12px}\n.ts{display:block;font-size:13px;color:#6b655d;font-variant-numeric:tabular-nums}\n.tile.lit{background:#fff7e6;border-color:#f8d48a;box-shadow:0 6px 20px rgba(245,165,36,.25)}\n.tile.lit .ic{background:#f5a524;color:#fff}\n.tile.lit .ts{color:#8a5a00;font-weight:600}\n.tile.unlocked .ic{background:#fde4df;color:#c4321f}\n.tile.unlocked .ts{color:#b02a17;font-weight:600}\n.tile.locked .ic{background:#dcf1e2;color:#2f8a4f}\n.tile.locked .ts{color:#2a7a46;font-weight:600}\n.top{display:flex;align-items:flex-start;justify-content:space-between;gap:6px}\n.wide .chg{position:absolute;top:12px;right:12px}\n.chg{white-space:nowrap;font-size:12px;font-weight:600;color:#7a4f00;background:#fdecc8;border-radius:10px;padding:2px 9px;opacity:0;transform:scale(.8);transition:opacity .18s,transform .18s}\n.tile.changed .chg{opacity:1;transform:none}\n.heat{display:flex;align-items:center;gap:12px}\n.heat .tx{flex:1}\n.deg{font-size:30px;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1.1}\n.ctl{display:flex;align-items:center;gap:8px;margin-top:12px}\n.step{width:48px;height:48px;border-radius:14px;border:0;background:#ebe7e1;font:inherit;font-size:24px;font-weight:600;color:#24221f;cursor:pointer;transition:background-color .15s,transform .15s}\n.away{flex:1;min-height:48px;border-radius:14px;border:1.5px solid #ddd6cc;background:#fff;font:inherit;font-weight:600;font-size:15px;color:#24221f;cursor:pointer;transition:background-color .18s,border-color .18s,color .18s,transform .15s}\n.away.on{background:#24221f;border-color:#24221f;color:#fff}\n.wash{display:flex;align-items:center;gap:12px}\n.wash svg.r{width:44px;height:44px;transform:rotate(-90deg);flex:none}\n.main{width:100%;min-height:54px;font:inherit;font-weight:700;font-size:16px;border:0;border-radius:16px;background:#24221f;color:#fff;cursor:pointer;box-shadow:0 8px 22px rgba(36,34,31,.22);transition:transform .15s,opacity .2s}\n.main:disabled{opacity:.4;cursor:default;box-shadow:none}\n.alt{width:100%;min-height:48px;font:inherit;font-size:15px;border:0;background:none;color:#6b655d;cursor:pointer;margin-top:6px}\n.tile:disabled,.step:disabled,.away:disabled,.alt:disabled{opacity:.6;cursor:default}\n.status{text-align:center;font-size:14px;color:#6b655d;min-height:20px;margin:6px 0 0}\n</style>\n<div class=\"wrap\">\n  <div class=\"head\">\n    <div class=\"house\"><svg width=\"36\" height=\"36\" viewBox=\"0 0 36 36\" fill=\"none\" aria-hidden=\"true\"><path d=\"M5 17 18 6l13 11\" stroke=\"#24221f\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M9 15v14h18V15\" stroke=\"#24221f\" stroke-width=\"2.4\" stroke-linejoin=\"round\"/><rect x=\"15\" y=\"20\" width=\"6\" height=\"9\" rx=\"1.5\" fill=\"#f5a524\"/><circle cx=\"26\" cy=\"9\" r=\"2.4\" fill=\"#f5a524\"/></svg></div>\n    <div><h1 id=\"title\"></h1><p class=\"since\" id=\"since\"></p></div>\n  </div>\n  <div class=\"grid\" id=\"grid\"></div>\n  <button class=\"main\" id=\"go\"></button>\n  <button class=\"alt\" id=\"skip\"></button>\n  <p class=\"status\" id=\"status\" role=\"status\"></p>\n</div>\n<script>\n// The card's content. Change only this object.\n// answer.data: {\"lights_off\":[\"living\",\"kitchen\"],\"heating_c\":17,\"lock_door\":true,\"close_garage\":false}\n// \"now\" is how the house is; \"want\" is what I suggest. The card starts on my suggestions.\nvar CARD = {\n  title: 'You’ve left home. Tidy up the house?',\n  since: 'Nobody home since 08:42',\n  lights: [\n    {id: 'living', name: 'Living room lights', now: true, want: false},\n    {id: 'kitchen', name: 'Kitchen lights', now: true, want: false}\n  ],\n  heating: {name: 'Heating', now: 21.5, want: 17, away: 17, min: 12, max: 25, step: 0.5, away_label: 'Away 17°', hint: 'Back to 21.5° when you’re 20 min away'},\n  door: {name: 'Front door', now_locked: false, want_locked: true},\n  garage: {name: 'Garage', now_closed: true, want_closed: true},\n  washer: {name: 'Washing machine', status: 'Finishing in 12 min', pct: 0.84},\n  words: {on: 'On', off: 'Off', locked: 'Locked', unlocked: 'Unlocked', closed: 'Closed', open: 'Open', was: 'Was'},\n  skip: 'Leave everything as it is',\n  none: 'Nothing to change',\n  sending: 'Sending…',\n  preview: 'Preview only: open this card in Bridgit to answer.',\n  done: 'Done. The house is tidied up'\n};\n\nvar ICONS = {\n  bulb: 'M9 18h6M10 21h4M12 3a6 6 0 0 0-3.5 10.9c.6.5 1 1.2 1 2V16h5v-.1c0-.8.4-1.5 1-2A6 6 0 0 0 12 3Z',\n  lock: 'M7 11V8a5 5 0 0 1 10 0v3M6 11h12v9H6z',\n  unlock: 'M7 11V8a5 5 0 0 1 9.6-2M6 11h12v9H6z',\n  garage: 'M3 10 12 4l9 6v10H3zM7 20v-7h10v7M7 16h10',\n  flame: 'M12 3c1 4 5 5.5 5 10a5 5 0 0 1-10 0c0-2.5 1.5-3.5 2-5.5 1 1.5 1.5 2 2 2.5C11.5 8 11 5.5 12 3Z',\n  wash: 'M5 3h14v18H5zM5 7h14M12 10a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z'\n};\nvar sent = false, S = {}, NS = 'http://www.w3.org/2000/svg';\nfunction $(id) { return document.getElementById(id); }\nfunction el(tag, cls, txt) { var e = document.createElement(tag); if (cls) e.className = cls; if (txt != null) e.textContent = txt; return e; }\nfunction icon(name) {\n  var w = el('div', 'ic'), s = document.createElementNS(NS, 'svg'), p = document.createElementNS(NS, 'path');\n  s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('fill', 'none');\n  p.setAttribute('d', ICONS[name]); p.setAttribute('stroke', 'currentColor'); p.setAttribute('stroke-width', '2');\n  p.setAttribute('stroke-linecap', 'round'); p.setAttribute('stroke-linejoin', 'round');\n  s.appendChild(p); w.appendChild(s); return {wrap: w, path: p};\n}\nfunction tile(name, iconName, wide) {\n  var t = el(wide ? 'div' : 'button', 'tile' + (wide ? ' wide' : '')), i = icon(iconName);\n  var badge = el('span', 'chg');\n  if (wide) { t.appendChild(badge); t.appendChild(i.wrap); }\n  else { var top = el('div', 'top'); top.appendChild(i.wrap); top.appendChild(badge); t.appendChild(top); }\n  var txt = el('div'); var n = el('span', 'tn', name), s = el('span', 'ts');\n  txt.appendChild(n); txt.appendChild(s);\n  return {t: t, i: i, n: n, s: s, txt: txt, badge: badge};\n}\n$('title').textContent = CARD.title;\n$('since').textContent = CARD.since;\n$('skip').textContent = CARD.skip;\nvar grid = $('grid'), T = {};\n\nCARD.lights.forEach(function (l) {\n  S[l.id] = l.want;\n  var x = tile(l.name, 'bulb'); x.t.appendChild(x.txt); grid.appendChild(x.t); T[l.id] = x;\n  x.t.onclick = function () { if (!sent) { S[l.id] = !S[l.id]; update(); } };\n});\n\n// Heating (wide)\nvar H = CARD.heating; S.heat = H.want;\nvar hx = tile(H.name, 'flame', true), hrow = el('div', 'heat'), htx = el('div', 'tx');\nvar deg = el('div', 'deg'); hx.n.style.marginTop = '0';\nhtx.appendChild(hx.n); htx.appendChild(deg); htx.appendChild(hx.s);\nhx.t.removeChild(hx.i.wrap); hrow.appendChild(hx.i.wrap); hrow.appendChild(htx); hx.t.appendChild(hrow);\nvar ctl = el('div', 'ctl'), minus = el('button', 'step', '−'), plus = el('button', 'step', '+'), away = el('button', 'away', H.away_label);\nminus.setAttribute('aria-label', 'Lower'); plus.setAttribute('aria-label', 'Raise');\nctl.appendChild(minus); ctl.appendChild(away); ctl.appendChild(plus); hx.t.appendChild(ctl);\ngrid.appendChild(hx.t); T.heat = hx;\nminus.onclick = function () { if (!sent) { S.heat = Math.max(H.min, S.heat - H.step); update(); } };\nplus.onclick = function () { if (!sent) { S.heat = Math.min(H.max, S.heat + H.step); update(); } };\naway.onclick = function () { if (!sent) { S.heat = S.heat === H.away ? H.now : H.away; update(); } };\n\n// Door and garage\nS.door = CARD.door.want_locked; S.garage = CARD.garage.want_closed;\nvar dx = tile(CARD.door.name, 'lock'); dx.t.appendChild(dx.txt); grid.appendChild(dx.t); T.door = dx;\ndx.t.onclick = function () { if (!sent) { S.door = !S.door; update(); } };\nvar gx = tile(CARD.garage.name, 'garage'); gx.t.appendChild(gx.txt); grid.appendChild(gx.t); T.garage = gx;\ngx.t.onclick = function () { if (!sent) { S.garage = !S.garage; update(); } };\n\n// Washer (info only)\nvar W = CARD.washer, wx = el('div', 'tile wide'), wr = el('div', 'wash');\nvar ring = document.createElementNS(NS, 'svg'); ring.setAttribute('viewBox', '0 0 44 44'); ring.setAttribute('class', 'r');\n[['#ebe7e1', 0], ['#4f8fd6', 1]].forEach(function (c) {\n  var ci = document.createElementNS(NS, 'circle');\n  ci.setAttribute('cx', 22); ci.setAttribute('cy', 22); ci.setAttribute('r', 18); ci.setAttribute('fill', 'none');\n  ci.setAttribute('stroke', c[0]); ci.setAttribute('stroke-width', 5); ci.setAttribute('stroke-linecap', 'round');\n  if (c[1]) ci.setAttribute('stroke-dasharray', (113.1 * W.pct) + ' 113.1');\n  ring.appendChild(ci);\n});\nvar wt = el('div'); wt.appendChild(el('span', 'tn', W.name)); wt.appendChild(el('span', 'ts', W.status));\nwt.firstChild.style.marginTop = '0';\nwr.appendChild(ring); wr.appendChild(wt); wx.appendChild(wr); grid.appendChild(wx);\n\nfunction mark(x, changed, was) { x.t.classList.toggle('changed', changed); if (changed) x.badge.textContent = CARD.words.was + ' ' + was.toLowerCase(); }\nfunction changes() {\n  var n = 0;\n  CARD.lights.forEach(function (l) { if (S[l.id] !== l.now) n++; });\n  if (S.heat !== H.now) n++;\n  if (S.door !== CARD.door.now_locked) n++;\n  if (S.garage !== CARD.garage.now_closed) n++;\n  return n;\n}\nfunction update() {\n  var w = CARD.words;\n  CARD.lights.forEach(function (l) {\n    var x = T[l.id], on = S[l.id];\n    x.t.classList.toggle('lit', on); x.s.textContent = on ? w.on : w.off;\n    x.t.setAttribute('aria-pressed', String(on)); mark(x, on !== l.now, l.now ? w.on : w.off);\n  });\n  deg.textContent = S.heat.toFixed(1) + '°';\n  hx.s.textContent = S.heat === H.now ? '' : H.hint;\n  away.classList.toggle('on', S.heat === H.away); mark(hx, S.heat !== H.now, H.now.toFixed(1) + '°');\n  dx.t.classList.toggle('locked', S.door); dx.t.classList.toggle('unlocked', !S.door);\n  dx.s.textContent = S.door ? w.locked : w.unlocked; dx.i.path.setAttribute('d', ICONS[S.door ? 'lock' : 'unlock']);\n  mark(dx, S.door !== CARD.door.now_locked, CARD.door.now_locked ? w.locked : w.unlocked);\n  gx.t.classList.toggle('locked', S.garage); gx.t.classList.toggle('unlocked', !S.garage);\n  gx.s.textContent = S.garage ? w.closed : w.open; mark(gx, S.garage !== CARD.garage.now_closed, CARD.garage.now_closed ? w.closed : w.open);\n  var n = changes();\n  $('go').textContent = n ? 'Apply ' + n + ' change' + (n > 1 ? 's' : '') : CARD.none;\n  $('go').disabled = n === 0;\n}\n\nfunction send(data) {\n  if (sent) return;\n  var B = window.Bridgit || window.Bridget;\n  if (!B) { $('status').textContent = CARD.preview; return; }\n  sent = true;\n  document.querySelectorAll('button').forEach(function (b) { b.disabled = true; });\n  $('status').textContent = CARD.sending;\n  B.respond(JSON.stringify(data));\n}\n$('go').onclick = function () {\n  var d = {lights_off: [], lights_on: [], heating_c: S.heat === H.now ? null : S.heat, lock_door: S.door && !CARD.door.now_locked, close_garage: S.garage && !CARD.garage.now_closed};\n  CARD.lights.forEach(function (l) { if (S[l.id] !== l.now) (S[l.id] ? d.lights_on : d.lights_off).push(l.id); });\n  if (CARD.door.now_locked && !S.door) d.unlock_door = true;\n  if (CARD.garage.now_closed && !S.garage) d.open_garage = true;\n  send(d);\n};\n$('skip').onclick = function () { send({action: 'leave_as_is'}); };\ndocument.addEventListener('bridgit:answered', function (e) {\n  e.preventDefault();\n  $('title').textContent = '✓ ' + CARD.done; $('title').classList.add('done');\n  $('status').textContent = '';\n  setTimeout(function () { $('title').scrollIntoView({block: 'center', behavior: 'smooth'}); }, 400);\n});\nupdate();\n</script>\n",
  "expires_in_seconds": 1800
}
Raw JSONhtmlsmart-hometilesstepper
HTML cardHome

4 plants are thirsty today

A checklist of rich item cards with live meters, a running count and a per-item snooze chip: the pattern for any daily routine with a few things to tick off.

Owner sees
Six plants with soil-moisture meters, thirsty ones first, each with a round Watered tick and a Remind me tonight chip.
Then
Mark watered plants with today's date and reset their moisture estimate. remind_tonight: send a notice at 20:00 for those plants. skipped: ask again tomorrow morning.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "4 plants are thirsty today",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "watered": [
        "monty",
        "fern",
        "calathea"
      ],
      "remind_tonight": [
        "olive"
      ],
      "skipped": []
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 9.7 KB
{
  "kind": "html",
  "title": "4 plants are thirsty today",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#eef3ea;color:#1f2a20;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:13px;font-weight:600;letter-spacing:.04em;color:#5b6b58;margin:0 0 4px}\nh1{font-size:24px;line-height:1.2;margin:0 0 14px;letter-spacing:-.01em}\n.sum{display:flex;align-items:center;gap:12px;background:#fffdf7;border-radius:16px;padding:12px 14px;box-shadow:0 1px 2px rgba(47,93,58,.08),0 6px 18px -10px rgba(47,93,58,.25);margin-bottom:16px}\n.sum b{font-variant-numeric:tabular-nums;font-size:15px;white-space:nowrap}\n.track{flex:1;height:8px;border-radius:99px;background:#e2eadc;overflow:hidden}\n.track i{display:block;height:100%;width:0;background:#2f5d3a;border-radius:99px;transition:width .2s ease}\n.list{display:grid;gap:12px}\n.plant{background:#fffdf7;border-radius:18px;padding:14px;box-shadow:0 1px 2px rgba(47,93,58,.08),0 8px 20px -14px rgba(47,93,58,.35);transition:background .18s,box-shadow .18s}\n.plant.done{background:#f3f8ef;box-shadow:0 0 0 1.5px #b9d2b4 inset}\n.row{display:flex;gap:12px;align-items:center}\n.pic{flex:none;width:52px;height:52px;border-radius:50%;display:grid;place-content:center;padding-bottom:9px;font-size:24px;background:radial-gradient(circle at 50% 35%,#f1f6ec 0,#dfe9d7 70%);position:relative}\n.pic::after{content:\"\";position:absolute;left:14px;right:14px;bottom:6px;height:9px;border-radius:2px 2px 6px 6px;background:#c46a4a;opacity:.9;z-index:0}\n.info{flex:1;min-width:0}\n.name{font-weight:650;font-size:16px;margin:0}\n.name span{font-weight:450;color:#5b6b58}\n.light{font-size:13px;color:#5b6b58;margin:1px 0 0}\n.tick{flex:none;width:52px;height:52px;border-radius:50%;border:2px solid #b9d2b4;background:#fff;color:#2f5d3a;display:grid;place-content:center;cursor:pointer;transition:background .18s,border-color .18s,transform .15s}\n.tick svg{width:24px;height:24px;opacity:.35;transition:opacity .18s}\n.tick.on{background:#2f5d3a;border-color:#2f5d3a;color:#fff}\n.tick.on svg{opacity:1}\n.tick:active,.chip:active,.go:active{transform:scale(.98)}\n.meter{margin:12px 0 0}\n.mtrack{height:6px;border-radius:99px;background:#efe6dc;overflow:hidden}\n.mtrack i{display:block;height:100%;border-radius:99px;transition:width .2s}\n.mlab{display:flex;justify-content:space-between;font-size:12px;color:#6c7a69;margin-top:4px}\n.mlab .now{color:#9a4a2e;font-weight:650}\n.foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px;font-size:13px;color:#5b6b58}\n.chip{font:inherit;font-size:14px;font-weight:600;min-height:48px;padding:0 16px;border-radius:99px;border:1.5px solid #e3cbbd;background:#fbf3ec;color:#8a3f24;cursor:pointer;transition:background .18s,color .18s,border-color .18s,transform .15s}\n.chip.on{background:#c46a4a;border-color:#c46a4a;color:#fff}\n.badge{font-size:12px;font-weight:700;color:#9a4a2e;background:#f8e7dc;border-radius:99px;padding:3px 9px}\n.go{display:block;width:100%;margin-top:20px;font:inherit;font-weight:700;font-size:17px;min-height:54px;border:0;border-radius:16px;background:#2f5d3a;color:#fff;cursor:pointer;box-shadow:0 8px 20px -10px #2f5d3a;transition:opacity .18s,transform .15s}\n.go:disabled{opacity:.45}\n.status{text-align:center;font-size:14px;color:#5b6b58;min-height:20px;margin:10px 0 0}\nbutton:disabled{cursor:default}\n</style>\n<div class=\"wrap\">\n  <p class=\"eyebrow\" id=\"eyebrow\"></p>\n  <h1 id=\"title\"></h1>\n  <div class=\"sum\"><b id=\"count\"></b><div class=\"track\"><i id=\"fill\"></i></div></div>\n  <div class=\"list\" id=\"list\"></div>\n  <button class=\"go\" id=\"go\"></button>\n  <p class=\"status\" id=\"status\"></p>\n</div>\n<script>\n// answer.data: {\"watered\":[ids],\"remind_tonight\":[ids],\"skipped\":[thirsty ids left alone]}\n// The card's content. Change only this object.\nvar CARD = {\n  eyebrow: \"Sunday 2 Nov · Flat on Długa\",\n  title: \"4 plants are thirsty today\",\n  done: \"Done for today\",\n  remind: \"Remind me tonight\",\n  remindOn: \"Tonight 20:00\",\n  fine: \"Fine until\",\n  thirsty: \"Thirsty\",\n  levels: [\"Dry\", \"OK\", \"Moist\"],\n  confirm: \"✓ Logged. Your jungle thanks you\",\n  plants: [\n    { id: \"monty\", icon: \"🌿\", name: \"Monstera\", nick: \"Monty\", moisture: 12, light: \"☀️ Bright, indirect\", thirsty: true },\n    { id: \"fern\", icon: \"🌱\", name: \"Boston fern\", nick: \"Fern\", moisture: 18, light: \"⛅ Shade, humid\", thirsty: true },\n    { id: \"olive\", icon: \"🫒\", name: \"Olive tree\", nick: \"Olive\", moisture: 22, light: \"☀️ Full sun\", thirsty: true },\n    { id: \"calathea\", icon: \"🍃\", name: \"Calathea\", nick: \"Cleo\", moisture: 26, light: \"⛅ Medium light\", thirsty: true },\n    { id: \"pothos\", icon: \"🪴\", name: \"Pothos\", nick: \"Pip\", moisture: 58, light: \"⛅ Any light\", thirsty: false, next: \"Wed\" },\n    { id: \"cactus\", icon: \"🌵\", name: \"Cactus\", nick: \"Spike\", moisture: 74, light: \"☀️ Full sun\", thirsty: false, next: \"Sun 16 Nov\" }\n  ]\n};\n\nvar $ = function (id) { return document.getElementById(id); };\nvar state = {}, sent = false;\nvar plants = CARD.plants.slice().sort(function (a, b) { return (b.thirsty - a.thirsty) || (a.moisture - b.moisture); });\nvar thirsty = plants.filter(function (p) { return p.thirsty; });\n$(\"eyebrow\").textContent = CARD.eyebrow;\n$(\"title\").textContent = CARD.title;\nvar check = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12.5l4.5 4.5L19 7.5\"/></svg>';\n\nfunction el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }\n\nplants.forEach(function (p) {\n  state[p.id] = { watered: false, remind: false };\n  var card = el(\"div\", \"plant\"), row = el(\"div\", \"row\");\n  var pic = el(\"div\", \"pic\", p.icon);\n  var info = el(\"div\", \"info\"), name = el(\"p\", \"name\", p.name + \" \");\n  name.appendChild(el(\"span\", \"\", \"· \" + p.nick));\n  info.appendChild(name); info.appendChild(el(\"p\", \"light\", p.light));\n  var tick = el(\"button\", \"tick\"); tick.innerHTML = check;\n  tick.setAttribute(\"aria-label\", \"Watered \" + p.name);\n  row.appendChild(pic); row.appendChild(info); row.appendChild(tick); card.appendChild(row);\n\n  var meter = el(\"div\", \"meter\"), mt = el(\"div\", \"mtrack\"), mi = el(\"i\");\n  var lvl = p.moisture < 33 ? 0 : p.moisture < 66 ? 1 : 2;\n  mi.style.width = p.moisture + \"%\";\n  mi.style.background = [\"#c46a4a\", \"#8fb486\", \"#2f5d3a\"][lvl];\n  mt.appendChild(mi); meter.appendChild(mt);\n  var lab = el(\"div\", \"mlab\");\n  CARD.levels.forEach(function (l, i) { lab.appendChild(el(\"span\", i === lvl ? \"now\" : \"\", l)); });\n  meter.appendChild(lab); card.appendChild(meter);\n\n  var foot = el(\"div\", \"foot\"), chip = null;\n  if (p.thirsty) {\n    foot.appendChild(el(\"span\", \"badge\", CARD.thirsty + \" · \" + p.moisture + \"%\"));\n    chip = el(\"button\", \"chip\", CARD.remind);\n    chip.onclick = function () {\n      if (sent) return;\n      var s = state[p.id]; s.remind = !s.remind; if (s.remind) s.watered = false; paint();\n    };\n    foot.appendChild(chip);\n  } else {\n    foot.appendChild(el(\"span\", \"\", CARD.fine + \" \" + p.next + \" · \" + p.moisture + \"%\"));\n  }\n  card.appendChild(foot); $(\"list\").appendChild(card);\n\n  tick.onclick = function () {\n    if (sent) return;\n    var s = state[p.id]; s.watered = !s.watered; if (s.watered) s.remind = false; paint();\n  };\n  p.ui = { card: card, tick: tick, chip: chip };\n});\n\nfunction pick(key) { return plants.filter(function (p) { return state[p.id][key]; }).map(function (p) { return p.id; }); }\n\nfunction paint() {\n  plants.forEach(function (p) {\n    var s = state[p.id];\n    p.ui.card.classList.toggle(\"done\", s.watered);\n    p.ui.tick.classList.toggle(\"on\", s.watered);\n    p.ui.tick.setAttribute(\"aria-pressed\", s.watered);\n    if (p.ui.chip) { p.ui.chip.classList.toggle(\"on\", s.remind); p.ui.chip.textContent = s.remind ? \"⏰ \" + CARD.remindOn : CARD.remind; }\n  });\n  var n = thirsty.filter(function (p) { return state[p.id].watered; }).length;\n  $(\"count\").textContent = n + \" of \" + thirsty.length + \" watered\";\n  $(\"fill\").style.width = (n / thirsty.length * 100) + \"%\";\n  var w = pick(\"watered\").length, r = pick(\"remind\").length;\n  $(\"go\").textContent = CARD.done + (w || r ? \" · \" + [w ? w + \" watered\" : \"\", r ? r + \" tonight\" : \"\"].filter(Boolean).join(\", \") : \"\");\n}\npaint();\n\n$(\"go\").onclick = function () {\n  if (sent) return;\n  var bridge = window.Bridgit || window.Bridget;\n  if (!bridge) { $(\"status\").textContent = \"Preview only: open this card in Bridgit to answer.\"; return; }\n  sent = true;\n  var answer = {\n    watered: pick(\"watered\"),\n    remind_tonight: pick(\"remind\"),\n    skipped: thirsty.filter(function (p) { var s = state[p.id]; return !s.watered && !s.remind; }).map(function (p) { return p.id; })\n  };\n  document.querySelectorAll(\"button\").forEach(function (b) { b.disabled = true; });\n  $(\"status\").textContent = \"Sending…\";\n  (window.Bridgit || window.Bridget).respond(JSON.stringify(answer));\n};\n\ndocument.addEventListener(\"bridgit:answered\", function (e) {\n  e.preventDefault();\n  sent = true;\n  document.querySelectorAll(\"button\").forEach(function (b) { b.disabled = true; });\n  $(\"title\").textContent = CARD.confirm;\n  $(\"status\").textContent = \"\";\n  setTimeout(function () { $(\"title\").scrollIntoView({ block: \"center\", behavior: \"smooth\" }); }, 400);\n});\n</script>\n",
  "expires_in_seconds": 43200
}
Raw JSONhtmlchecklisthomereminder

Health

HTML cardHealth

Evening medicines, 21:00

A per-item Taken / Skip choice with reason chips and a week of history dots: a calm, clear way to log doses or any repeating to-do.

Owner sees
Three medicines with pill pictures, doses and notes, Taken / Skip buttons, and dots showing which evenings this week were taken.
Then
Log taken doses with the time. skipped with ran_out: add it to the shopping list or offer to reorder. side_effects: suggest noting it for the doctor. forgot_earlier or none: just log it and remind again tomorrow at 21:00.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Evening medicines, 21:00",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "taken": [
        "atorvastatin",
        "vitamin_d"
      ],
      "skipped": [
        {
          "id": "magnesium",
          "reason": "ran_out"
        }
      ],
      "at": "21:04"
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 9.3 KB
{
  "kind": "html",
  "title": "Evening medicines, 21:00",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#f4f1ff;color:#231d3d;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:13px;font-weight:600;color:#5d5580;margin:0 0 4px;letter-spacing:.02em}\nh1{font-size:24px;line-height:1.2;margin:0 0 16px;letter-spacing:-.01em}\n.box{background:#fff;border-radius:18px;padding:14px 16px;box-shadow:0 1px 2px rgba(60,40,140,.06),0 10px 24px -16px rgba(60,40,140,.35)}\n.week{margin-bottom:14px}\n.whead{display:flex;justify-content:space-between;font-size:14px;margin-bottom:10px}\n.whead b{font-weight:650}\n.whead span{color:#5d5580;font-variant-numeric:tabular-nums}\n.days{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-size:12px;color:#5d5580}\n.dot{width:22px;height:22px;border-radius:50%;margin:0 auto 4px;border:2px solid #c9bff5;background:transparent}\n.dot.t{background:#7c5cff;border-color:#7c5cff}\n.dot.now{border:2px dashed #7c5cff}\n.days .today{color:#5536e0;font-weight:700}\n.list{display:grid;gap:12px}\n.med{transition:box-shadow .18s}\n.top{display:flex;gap:14px;align-items:center}\n.pillbox{flex:none;width:56px;height:56px;border-radius:16px;background:#f6f3ff;display:grid;place-content:center}\n.pill{display:block}\n.cap{width:40px;height:16px;border-radius:99px;transform:rotate(-35deg);box-shadow:inset 0 -2px 0 rgba(0,0,0,.08),0 0 0 1px rgba(40,60,140,.18)}\n.tab{width:30px;height:30px;border-radius:50%;position:relative;box-shadow:inset 0 -2px 0 rgba(0,0,0,.08),0 0 0 1px rgba(0,0,0,.06)}\n.tab::after{content:\"\";position:absolute;left:6px;right:6px;top:14px;height:2px;border-radius:2px;background:rgba(0,0,0,.12)}\n.gel{width:34px;height:22px;border-radius:50%;position:relative;transform:rotate(-20deg)}\n.gel::after{content:\"\";position:absolute;left:6px;top:4px;width:9px;height:5px;border-radius:50%;background:rgba(255,255,255,.7)}\n.name{font-weight:650;margin:0}\n.dose{font-size:14px;color:#3f3763;margin:1px 0 0}\n.note{font-size:13px;color:#6b6390;margin:2px 0 0}\n.acts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}\n.btn{font:inherit;font-weight:650;min-height:48px;border-radius:14px;border:1.5px solid #e2dcfa;background:#fff;color:#3f3763;cursor:pointer;transition:background .18s,border-color .18s,color .18s,transform .15s}\n.btn.taken.on{background:#dff7ec;border-color:#5fcf9f;color:#0f6b45}\n.btn.skip.on{background:#f1edff;border-color:#7c5cff;color:#4a2fd1}\n.btn:active,.chip:active,.go:active{transform:scale(.98)}\n.why{display:none;flex-wrap:wrap;gap:8px;margin-top:12px}\n.why.open{display:flex}\n.why p{width:100%;margin:0;font-size:13px;color:#6b6390}\n.chip{font:inherit;font-size:14px;min-height:48px;padding:0 14px;border-radius:99px;border:1.5px solid #e2dcfa;background:#faf9ff;color:#3f3763;cursor:pointer;transition:background .18s,color .18s,border-color .18s,transform .15s}\n.chip.on{background:#7c5cff;border-color:#7c5cff;color:#fff}\n.go{display:block;width:100%;margin-top:20px;font:inherit;font-weight:700;font-size:17px;min-height:54px;border:0;border-radius:16px;background:#6a48f5;color:#fff;cursor:pointer;box-shadow:0 10px 22px -12px #6a48f5;transition:opacity .18s,transform .15s}\n.go:disabled{opacity:.45;box-shadow:none}\n.status{text-align:center;font-size:14px;color:#5d5580;min-height:20px;margin:10px 0 0}\nbutton:disabled{cursor:default}\n</style>\n<div class=\"wrap\">\n  <p class=\"eyebrow\" id=\"eyebrow\"></p>\n  <h1 id=\"title\"></h1>\n  <div class=\"box week\">\n    <div class=\"whead\"><b id=\"weekLabel\"></b><span id=\"weekCount\"></span></div>\n    <div class=\"days\" id=\"days\"></div>\n  </div>\n  <div class=\"list\" id=\"list\"></div>\n  <button class=\"go\" id=\"go\" disabled></button>\n  <p class=\"status\" id=\"status\"></p>\n</div>\n<script>\n// answer.data: {\"taken\":[ids],\"skipped\":[{\"id\",\"reason\"}],\"at\":\"HH:MM\"}; reason is null if none was picked\n// The card's content. Change only this object.\nvar CARD = {\n  eyebrow: \"Sunday 2 Nov · 21:00 reminder\",\n  title: \"Evening medicines, 21:00\",\n  weekLabel: \"This week\",\n  days: [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"],\n  history: [1, 1, 0, 1, 1, 1],\n  taken: \"Taken\",\n  skip: \"Skip\",\n  whyLabel: \"Why skip it?\",\n  reasons: [\n    { id: \"forgot_earlier\", label: \"Forgot earlier\" },\n    { id: \"side_effects\", label: \"Side effects\" },\n    { id: \"ran_out\", label: \"Ran out\" }\n  ],\n  save: \"Save evening dose\",\n  confirm: \"✓ Evening dose saved. Sleep well\",\n  meds: [\n    { id: \"atorvastatin\", name: \"Atorvastatin\", dose: \"20 mg · 1 tablet\", note: \"With water, after food\", shape: \"tab\", colors: [\"#ffd9e4\"] },\n    { id: \"vitamin_d\", name: \"Vitamin D3\", dose: \"2000 IU · 1 softgel\", note: \"With your evening meal\", shape: \"gel\", colors: [\"#ffc93d\"] },\n    { id: \"magnesium\", name: \"Magnesium\", dose: \"250 mg · 1 capsule\", note: \"An hour before bed\", shape: \"cap\", colors: [\"#6aa8ff\", \"#f2f5ff\"] }\n  ]\n};\n\nvar $ = function (id) { return document.getElementById(id); };\nfunction el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }\nvar choice = {}, sent = false;\n$(\"eyebrow\").textContent = CARD.eyebrow;\n$(\"title\").textContent = CARD.title;\n$(\"weekLabel\").textContent = CARD.weekLabel;\nvar done = CARD.history.filter(Boolean).length;\n$(\"weekCount\").textContent = done + \" of \" + CARD.history.length + \" evenings\";\n\nCARD.days.forEach(function (d, i) {\n  var col = el(\"div\", i === CARD.history.length ? \"today\" : \"\"), dot = el(\"div\", \"dot\");\n  if (i < CARD.history.length) dot.classList.add(CARD.history[i] ? \"t\" : \"m\");\n  else if (i === CARD.history.length) dot.classList.add(\"now\");\n  col.appendChild(dot); col.appendChild(document.createTextNode(d)); $(\"days\").appendChild(col);\n});\n\nCARD.meds.forEach(function (m) {\n  var card = el(\"div\", \"box med\"), top = el(\"div\", \"top\");\n  var box = el(\"div\", \"pillbox\"), pill = el(\"i\", \"pill \" + m.shape);\n  pill.style.background = m.colors.length > 1 ? \"linear-gradient(90deg,\" + m.colors[0] + \" 50%,\" + m.colors[1] + \" 50%)\" : m.colors[0];\n  box.appendChild(pill); top.appendChild(box);\n  var info = el(\"div\", \"\");\n  info.appendChild(el(\"p\", \"name\", m.name));\n  info.appendChild(el(\"p\", \"dose\", m.dose));\n  info.appendChild(el(\"p\", \"note\", m.note));\n  top.appendChild(info); card.appendChild(top);\n\n  var acts = el(\"div\", \"acts\"), bt = el(\"button\", \"btn taken\", \"✓ \" + CARD.taken), bs = el(\"button\", \"btn skip\", CARD.skip);\n  acts.appendChild(bt); acts.appendChild(bs); card.appendChild(acts);\n  var why = el(\"div\", \"why\"); why.appendChild(el(\"p\", \"\", CARD.whyLabel));\n  var chips = CARD.reasons.map(function (r) {\n    var c = el(\"button\", \"chip\", r.label);\n    c.onclick = function () {\n      if (sent) return;\n      choice[m.id].reason = choice[m.id].reason === r.id ? null : r.id; paint();\n    };\n    why.appendChild(c); return c;\n  });\n  card.appendChild(why); $(\"list\").appendChild(card);\n\n  bt.onclick = function () { if (!sent) { choice[m.id] = { v: \"taken\" }; paint(); } };\n  bs.onclick = function () { if (!sent) { choice[m.id] = { v: \"skip\", reason: (choice[m.id] || {}).reason || null }; paint(); } };\n  m.ui = { bt: bt, bs: bs, why: why, chips: chips };\n});\n\nfunction paint() {\n  var t = 0, s = 0;\n  CARD.meds.forEach(function (m) {\n    var c = choice[m.id] || {};\n    m.ui.bt.classList.toggle(\"on\", c.v === \"taken\");\n    m.ui.bs.classList.toggle(\"on\", c.v === \"skip\");\n    m.ui.why.classList.toggle(\"open\", c.v === \"skip\");\n    m.ui.chips.forEach(function (ch, i) { ch.classList.toggle(\"on\", c.reason === CARD.reasons[i].id); });\n    if (c.v === \"taken\") t++; if (c.v === \"skip\") s++;\n  });\n  var left = CARD.meds.length - t - s;\n  $(\"go\").disabled = left > 0;\n  $(\"go\").textContent = left > 0 ? CARD.save + \" · \" + left + \" to go\" : CARD.save + \" · \" + t + \" taken\" + (s ? \", \" + s + \" skipped\" : \"\");\n}\npaint();\n\n$(\"go\").onclick = function () {\n  if (sent) return;\n  var bridge = window.Bridgit || window.Bridget;\n  if (!bridge) { $(\"status\").textContent = \"Preview only: open this card in Bridgit to answer.\"; return; }\n  sent = true;\n  var now = new Date(), pad = function (n) { return (n < 10 ? \"0\" : \"\") + n; };\n  var answer = { taken: [], skipped: [], at: pad(now.getHours()) + \":\" + pad(now.getMinutes()) };\n  CARD.meds.forEach(function (m) {\n    var c = choice[m.id];\n    if (c.v === \"taken\") answer.taken.push(m.id); else answer.skipped.push({ id: m.id, reason: c.reason || null });\n  });\n  document.querySelectorAll(\"button\").forEach(function (b) { b.disabled = true; });\n  $(\"status\").textContent = \"Sending…\";\n  (window.Bridgit || window.Bridget).respond(JSON.stringify(answer));\n};\n\ndocument.addEventListener(\"bridgit:answered\", function (e) {\n  e.preventDefault();\n  sent = true;\n  document.querySelectorAll(\"button\").forEach(function (b) { b.disabled = true; });\n  $(\"title\").textContent = CARD.confirm;\n  $(\"status\").textContent = \"\";\n  setTimeout(function () { $(\"title\").scrollIntoView({ block: \"center\", behavior: \"smooth\" }); }, 400);\n});\n</script>\n",
  "expires_in_seconds": 7200
}
Raw JSONhtmlhealthreminderhistory
HTML cardHealth

Your week: 3 habits, 16 of 21 days

A weekly review with a score ring, tappable day grids the owner can correct and a goal stepper per row: good for any streak or tracker check-in.

Owner sees
A dark weekly summary with a 16/21 ring, each habit's Mon–Sun squares and streak, and a stepper to set next week's goal.
Then
Save the corrected days as the week's record and set the new goals for Monday. Mention any habit whose goal went up or that dropped below its goal, once, without nagging.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Your week: 3 habits, 16 of 21 days",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "days": {
        "walk": [
          1,
          1,
          1,
          0,
          1,
          1,
          1
        ],
        "read": [
          1,
          0,
          1,
          1,
          1,
          0,
          1
        ],
        "phone": [
          1,
          1,
          0,
          1,
          1,
          1,
          1
        ]
      },
      "goals": {
        "walk": 6,
        "read": 5,
        "phone": 5
      }
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 8.5 KB
{
  "kind": "html",
  "title": "Your week: 3 habits, 16 of 21 days",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#0b1020;color:#eef1fb;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.head{display:flex;align-items:center;gap:16px;margin-bottom:18px}\n.ring{flex:none;position:relative;width:96px;height:96px}\n.ring svg{width:96px;height:96px;transform:rotate(-90deg)}\n.ring circle{fill:none;stroke-width:9}\n.ring .bg{stroke:#1f2740}\n.ring .fg{stroke:url(#rg);stroke-linecap:round;transition:stroke-dashoffset .3s ease}\n.ring b{position:absolute;inset:0;display:grid;place-content:center;font-size:22px;font-weight:750;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n.eyebrow{font-size:13px;font-weight:600;color:#9aa3c2;margin:0 0 4px}\nh1{font-size:21px;line-height:1.25;margin:0;letter-spacing:-.01em}\n.list{display:grid;gap:12px}\n.habit{background:#141a2e;border:1px solid #1f2740;border-radius:18px;padding:14px}\n.hrow{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:10px}\n.hname{font-weight:650;margin:0}\n.streak{font-size:13px;color:#c3c9de;white-space:nowrap;font-variant-numeric:tabular-nums}\n.streak svg{width:13px;height:13px;vertical-align:-1px;margin-right:3px}\n.days{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}\n.day{font:inherit;font-size:12px;font-weight:650;min-height:48px;border-radius:12px;border:1.5px solid #2b3452;background:transparent;color:#8d96b5;cursor:pointer;padding:0;transition:background .18s,border-color .18s,color .18s,transform .15s}\n.day.on{border-color:transparent;background:linear-gradient(140deg,var(--a),var(--b));color:#0b1020}\n.day:active,.step:active,.go:active{transform:scale(.96)}\n.goal{display:flex;align-items:center;justify-content:space-between;margin-top:12px;padding-top:12px;border-top:1px solid #1f2740;font-size:14px;color:#c3c9de}\n.stepper{display:flex;align-items:center;gap:4px}\n.step{font:inherit;font-size:22px;width:48px;height:48px;border-radius:14px;border:0;background:#1f2740;color:#eef1fb;cursor:pointer;transition:transform .15s,opacity .15s}\n.step:disabled{opacity:.35}\n.gval{min-width:68px;text-align:center;font-weight:650;color:#eef1fb;font-variant-numeric:tabular-nums}\n.go{display:block;width:100%;margin-top:20px;font:inherit;font-weight:700;font-size:17px;min-height:54px;border:0;border-radius:16px;background:linear-gradient(120deg,#2dd4bf,#8b5cf6 55%,#f472b6);color:#0b1020;cursor:pointer;transition:opacity .18s,transform .15s}\n.go:disabled{opacity:.45}\n.hint{font-size:13px;color:#9aa3c2;text-align:center;margin:12px 0 0}\n.status{text-align:center;font-size:14px;color:#9aa3c2;min-height:20px;margin:10px 0 0}\nbutton:disabled{cursor:default}\n</style>\n<div class=\"wrap\">\n  <div class=\"head\">\n    <div class=\"ring\">\n      <svg viewBox=\"0 0 96 96\" aria-hidden=\"true\">\n        <defs><linearGradient id=\"rg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#2dd4bf\"/><stop offset=\".55\" stop-color=\"#8b5cf6\"/><stop offset=\"1\" stop-color=\"#f472b6\"/></linearGradient></defs>\n        <circle class=\"bg\" cx=\"48\" cy=\"48\" r=\"40\"/><circle class=\"fg\" id=\"arc\" cx=\"48\" cy=\"48\" r=\"40\"/>\n      </svg>\n      <b id=\"score\"></b>\n    </div>\n    <div><p class=\"eyebrow\" id=\"eyebrow\"></p><h1 id=\"title\"></h1></div>\n  </div>\n  <div class=\"list\" id=\"list\"></div>\n  <p class=\"hint\" id=\"hint\"></p>\n  <button class=\"go\" id=\"go\"></button>\n  <p class=\"status\" id=\"status\"></p>\n</div>\n<script>\n// answer.data: {\"days\":{habit_id:[Mon..Sun as 1/0]},\"goals\":{habit_id:days per week}}\n// The card's content. Change only this object.\nvar CARD = {\n  eyebrow: \"Week of 27 Oct – 2 Nov\",\n  title: \"Your week: 3 habits, 16 of 21 days\",\n  days: [\"M\", \"T\", \"W\", \"T\", \"F\", \"S\", \"S\"],\n  dayNames: [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"],\n  streak: \"day streak\",\n  noStreak: \"Start again tonight\",\n  goal: \"Next week's goal\",\n  unit: \"days\",\n  hint: \"Tap a day to fix it.\",\n  save: \"Save the week\",\n  confirm: \"✓ Week saved. New goals start Monday\",\n  habits: [\n    { id: \"walk\", name: \"Walk 8k steps\", colors: [\"#2dd4bf\", \"#22d3ee\"], done: [1, 1, 1, 0, 1, 1, 1], goal: 6 },\n    { id: \"read\", name: \"Read 20 min\", colors: [\"#a78bfa\", \"#f472b6\"], done: [1, 0, 1, 1, 0, 0, 1], goal: 5 },\n    { id: \"phone\", name: \"No phone after 23:00\", colors: [\"#fbbf24\", \"#fb7185\"], done: [1, 1, 0, 1, 1, 1, 1], goal: 4 }\n  ]\n};\n\nvar $ = function (id) { return document.getElementById(id); };\nfunction el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }\nvar sent = false, C = 2 * Math.PI * 40;\nvar flame = '<svg viewBox=\"0 0 24 24\"><path fill=\"#fb923c\" d=\"M12 2c1 4 6 6 6 12a6 6 0 0 1-12 0c0-3 2-5 3-6 0 2 1 3 2 3 0-3-1-6 1-9z\"/></svg>';\n$(\"eyebrow\").textContent = CARD.eyebrow;\n$(\"title\").textContent = CARD.title;\n$(\"hint\").textContent = CARD.hint;\n$(\"arc\").style.strokeDasharray = C;\n\nvar habits = CARD.habits.map(function (h) {\n  var x = { id: h.id, done: h.done.slice(), goal: h.goal };\n  var card = el(\"div\", \"habit\"), row = el(\"div\", \"hrow\");\n  row.appendChild(el(\"p\", \"hname\", h.name));\n  var st = el(\"span\", \"streak\"); row.appendChild(st); card.appendChild(row);\n  card.style.setProperty(\"--a\", h.colors[0]); card.style.setProperty(\"--b\", h.colors[1]);\n  var days = el(\"div\", \"days\");\n  x.btns = CARD.days.map(function (d, i) {\n    var b = el(\"button\", \"day\", d);\n    b.onclick = function () { if (!sent) { x.done[i] = x.done[i] ? 0 : 1; paint(); } };\n    days.appendChild(b); return b;\n  });\n  card.appendChild(days);\n  var goal = el(\"div\", \"goal\"); goal.appendChild(el(\"span\", \"\", CARD.goal));\n  var sp = el(\"div\", \"stepper\"), minus = el(\"button\", \"step\", \"−\"), val = el(\"span\", \"gval\"), plus = el(\"button\", \"step\", \"+\");\n  minus.setAttribute(\"aria-label\", \"Fewer days\"); plus.setAttribute(\"aria-label\", \"More days\");\n  minus.onclick = function () { if (!sent && x.goal > 1) { x.goal--; paint(); } };\n  plus.onclick = function () { if (!sent && x.goal < 7) { x.goal++; paint(); } };\n  sp.appendChild(minus); sp.appendChild(val); sp.appendChild(plus); goal.appendChild(sp); card.appendChild(goal);\n  $(\"list\").appendChild(card);\n  x.ui = { st: st, val: val, minus: minus, plus: plus, name: h.name };\n  return x;\n});\n\nfunction paint() {\n  var total = 0, max = 0;\n  habits.forEach(function (x) {\n    x.btns.forEach(function (b, i) {\n      b.classList.toggle(\"on\", !!x.done[i]);\n      b.setAttribute(\"aria-label\", x.ui.name + \", \" + CARD.dayNames[i] + (x.done[i] ? \", done\" : \", missed\"));\n    });\n    var s = 0; for (var i = x.done.length - 1; i >= 0 && x.done[i]; i--) s++;\n    x.ui.st.innerHTML = s ? flame : \"\";\n    x.ui.st.appendChild(document.createTextNode(s ? s + \" \" + CARD.streak : CARD.noStreak));\n    x.ui.val.textContent = x.goal + \" \" + CARD.unit;\n    if (!sent) { x.ui.minus.disabled = x.goal <= 1; x.ui.plus.disabled = x.goal >= 7; }\n    total += x.done.reduce(function (a, b) { return a + b; }, 0); max += x.done.length;\n  });\n  $(\"score\").textContent = total + \"/\" + max;\n  $(\"arc\").style.strokeDashoffset = C * (1 - total / max);\n  $(\"go\").textContent = CARD.save + \" · \" + total + \" of \" + max;\n}\npaint();\n\n$(\"go\").onclick = function () {\n  if (sent) return;\n  var bridge = window.Bridgit || window.Bridget;\n  if (!bridge) { $(\"status\").textContent = \"Preview only: open this card in Bridgit to answer.\"; return; }\n  sent = true;\n  var answer = { days: {}, goals: {} };\n  habits.forEach(function (x) { answer.days[x.id] = x.done.slice(); answer.goals[x.id] = x.goal; });\n  document.querySelectorAll(\"button\").forEach(function (b) { b.disabled = true; });\n  $(\"status\").textContent = \"Sending…\";\n  (window.Bridgit || window.Bridget).respond(JSON.stringify(answer));\n};\n\ndocument.addEventListener(\"bridgit:answered\", function (e) {\n  e.preventDefault();\n  sent = true;\n  document.querySelectorAll(\"button\").forEach(function (b) { b.disabled = true; });\n  $(\"title\").textContent = CARD.confirm;\n  $(\"status\").textContent = \"\";\n  setTimeout(function () { $(\"title\").scrollIntoView({ block: \"center\", behavior: \"smooth\" }); }, 400);\n});\n</script>\n",
  "expires_in_seconds": 172800
}
Raw JSONhtmlhabitsstepperweekly

Work & paperwork

HTML cardWork & paperwork

Sign the lease for Flat 4B?

A long document with key facts on top and flagged clauses. Approval unlocks only after scrolling to the end. The tap records approval; the agent does the actual signing.

Owner sees
Rent, deposit and dates as tiles, the full lease with two flagged clauses, a progress bar, a name field, Approve signing and a note that the lease has not been submitted yet.
Then
approve_signing: submit the signature to the agency with the two flagged clauses as questions, then send one notice saying whether it went through. not_yet: ask what is holding them back.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Sign the lease for Flat 4B?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "decision": "approve_signing",
      "typed_name": "Alex Morgan",
      "scrolled_to_end": true,
      "read_seconds": 94
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 6.2 KB
{
  "kind": "html",
  "title": "Sign the lease for Flat 4B?",
  "expires_in_seconds": 172800,
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#f4f1ea;color:#1f1d19;font:16px/1.5 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:680px;margin:0 auto;padding:20px 18px 170px}\n.tag{display:inline-block;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#7a5b2e;background:#efe2c9;border-radius:99px;padding:3px 10px}\nh1{font-size:24px;line-height:1.2;margin:10px 0 4px}\n.sub{color:#6b665c;margin:0 0 16px}\n.facts{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 18px}\n.facts div{background:#fff;border-radius:12px;padding:10px 12px;box-shadow:0 1px 0 #e4ded2}\n.facts b{display:block;font-size:18px}\n.facts span{font-size:13px;color:#6b665c}\n.doc{background:#fff;border-radius:16px;padding:18px 18px 8px;box-shadow:0 1px 0 #e4ded2}\n.doc h2{font-size:15px;margin:16px 0 4px;color:#3a352c}\n.doc h2:first-child{margin-top:0}\n.doc p{margin:0 0 10px;font-size:15px;color:#3a352c}\n.flag{background:#fff6dc;border-left:4px solid #e0a82e;padding:8px 10px;border-radius:8px}\n.end{text-align:center;color:#8a8478;font-size:13px;padding:10px 0 4px}\n.bar{position:fixed;left:0;right:0;bottom:0;background:#fffdf8;border-top:1px solid #e4ded2;padding:12px 18px 12px}\n.bar .in{max-width:680px;margin:0 auto}\n.prog{height:4px;background:#ece6da;border-radius:4px;overflow:hidden;margin-bottom:10px}\n.prog i{display:block;height:100%;width:0;background:#2f7d5b;transition:width .2s}\nlabel.name{display:flex;gap:8px;align-items:center;font-size:14px;color:#6b665c;margin-bottom:10px}\nlabel.name input{flex:1;font:inherit;font-size:20px;font-family:\"Snell Roundhand\",\"Segoe Script\",\"Brush Script MT\",cursive;border:0;border-bottom:2px solid #c9c1b1;background:transparent;padding:4px 2px;color:#1f1d19;min-width:0}\n.row{display:flex;gap:10px}\nbutton{font:inherit;font-weight:600;border:0;border-radius:12px;padding:14px 16px;min-height:48px;cursor:pointer}\n.sign{flex:1;background:#2f7d5b;color:#fff}\n.sign:disabled{background:#b9c9bf;color:#f4f7f5}\n.later{background:#ece6da;color:#3a352c}\n.hint{font-size:13px;color:#8a8478;margin:6px 0 0;text-align:center}\n</style>\n<div class=\"wrap\">\n  <span class=\"tag\">Lease · needs your approval</span>\n  <h1>12-month lease, Flat 4B, 18 Orchard Street</h1>\n  <p class=\"sub\">From Maple &amp; Lane Lettings. I read it and flagged two clauses. Scroll to the end to approve signing.</p>\n  <div class=\"facts\">\n    <div><b>€1,450</b><span>per month, due the 1st</span></div>\n    <div><b>€2,900</b><span>deposit, held in escrow</span></div>\n    <div><b>1 Dec 2026</b><span>move-in date</span></div>\n    <div><b>2 months</b><span>notice to leave</span></div>\n  </div>\n  <div class=\"doc\" id=\"doc\">\n    <h2>1. Parties</h2>\n    <p>This agreement is between Maple &amp; Lane Lettings on behalf of the owner (\"Landlord\") and you (\"Tenant\") for Flat 4B, 18 Orchard Street, including the storage cage in the basement.</p>\n    <h2>2. Term</h2>\n    <p>The tenancy runs for 12 months from 1 December 2026. After that it continues month to month until either side gives notice.</p>\n    <h2>3. Rent</h2>\n    <p>Rent is €1,450 per month, paid by bank transfer on or before the 1st. Rent can rise once a year by no more than the published inflation index.</p>\n    <h2>4. Deposit</h2>\n    <p>The deposit of €2,900 is held by an approved deposit scheme and returned within 14 days of the end of the tenancy, less agreed deductions.</p>\n    <p class=\"flag\"><b>Flagged:</b> deductions include \"professional cleaning\" at a fixed €250 whatever the condition. You can ask to strike this.</p>\n    <h2>5. Repairs</h2>\n    <p>The Landlord repairs the structure, heating, hot water and appliances supplied. The Tenant reports problems promptly and allows access with 24 hours' notice.</p>\n    <h2>6. Pets</h2>\n    <p>One cat or small dog is allowed with written consent, which will not be unreasonably refused.</p>\n    <h2>7. Subletting</h2>\n    <p>The Tenant may not sublet the whole flat. Short stays by guests of up to 14 nights a year are fine.</p>\n    <p class=\"flag\"><b>Flagged:</b> a €95 \"admin fee\" applies to every renewal. Most agents in the city no longer charge this.</p>\n    <h2>8. Ending the tenancy</h2>\n    <p>After the first 6 months either side may end the tenancy with 2 months' written notice. The flat is returned clean, with the keys and fobs.</p>\n    <h2>9. Signatures</h2>\n    <p>By signing, the Tenant confirms they have read and agree to all clauses above.</p>\n    <div class=\"end\">End of document</div>\n  </div>\n</div>\n<div class=\"bar\"><div class=\"in\">\n  <div class=\"prog\"><i id=\"p\"></i></div>\n  <label class=\"name\">Type your name <input id=\"n\" maxlength=\"80\" autocomplete=\"name\" placeholder=\"Your full name\"></label>\n  <div class=\"row\">\n    <button class=\"later\" id=\"later\">Not yet</button>\n    <button class=\"sign\" id=\"sign\" disabled>Approve signing</button>\n  </div>\n  <p class=\"hint\" id=\"h\">Scroll to the end to unlock approval</p>\n  <p class=\"hint\">This sends your approval to the agent. The lease has not been submitted yet.</p>\n</div></div>\n<script>\nvar start=Date.now(),reached=false,sent=false,sign=document.getElementById('sign'),nm=document.getElementById('n'),hint=document.getElementById('h');\nfunction update(){\n  var el=document.scrollingElement||document.documentElement,max=el.scrollHeight-el.clientHeight;\n  var f=max<=0?1:Math.min(1,el.scrollTop/max);\n  document.getElementById('p').style.width=(f*100)+'%';\n  if(f>0.97)reached=true;\n  sign.disabled=!(reached&&nm.value.trim().length>1);\n  hint.textContent=reached?(nm.value.trim().length>1?'Ready to approve':'Type your name to approve'):'Scroll to the end to unlock approval';\n}\nwindow.addEventListener('scroll',update,{passive:true});nm.addEventListener('input',update);update();\nfunction send(o){if(sent)return;sent=true;o.read_seconds=Math.round((Date.now()-start)/1000);(window.Bridgit||window.Bridget).respond(JSON.stringify(o));}\nsign.onclick=function(){send({decision:'approve_signing',typed_name:nm.value.trim(),scrolled_to_end:reached})};\ndocument.getElementById('later').onclick=function(){send({decision:'not_yet',scrolled_to_end:reached})};\n</script>\n"
}
Raw JSONhtmlscrollsignature
HTML cardWork & paperwork

Which photo should lead the listing?

A photo grid with the agent’s own pick marked. Drawn with inline SVG, so it runs offline.

Owner sees
Six photos in a grid (three across on a tablet), My pick on one, and Use as cover.
Then
Reorder the listing photos and publish.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Which photo should lead the listing?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "cover": "balcony",
      "agreed_with_suggestion": false,
      "order": [
        "balcony",
        "living_room",
        "kitchen",
        "bedroom",
        "bathroom",
        "street"
      ]
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 8.2 KB
{
  "kind": "html",
  "title": "Which photo should lead the listing?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#101418;color:#eef2f5;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n.wrap{max-width:900px;margin:0 auto;padding:20px 16px 110px}\nh1{font-size:23px;line-height:1.2;margin:0 0 4px}\n.sub{color:#9fb0bd;margin:0 0 16px}\n.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}\n@media(min-width:720px){.grid{grid-template-columns:repeat(3,1fr)}}\n.ph{position:relative;border-radius:14px;overflow:hidden;background:#1b2229;border:3px solid transparent;cursor:pointer;padding:0;color:inherit;text-align:left;font:inherit}\n.ph svg{display:block;width:100%;height:auto;aspect-ratio:4/3}\n.ph .cap{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;font-size:14px}\n.ph .cap small{color:#9fb0bd}\n.ph.on{border-color:#4cc38a}\n.badge{position:absolute;top:8px;left:8px;background:#4cc38a;color:#06210f;font-size:12px;font-weight:700;border-radius:99px;padding:3px 9px;display:none}\n.ph.on .badge{display:block}\n.ai{position:absolute;top:8px;right:8px;background:rgba(0,0,0,.55);font-size:12px;border-radius:99px;padding:3px 9px}\n.bar{position:fixed;left:0;right:0;bottom:0;background:#151b21;border-top:1px solid #26303a;padding:12px 16px 12px}\n.bar .in{max-width:900px;margin:0 auto;display:flex;gap:10px;align-items:center}\n.bar p{flex:1;margin:0;font-size:14px;color:#9fb0bd}\nbutton.go{font:inherit;font-weight:700;border:0;border-radius:12px;padding:14px 20px;min-height:48px;background:#4cc38a;color:#06210f;cursor:pointer}\nbutton.go:disabled{opacity:.4}\n</style>\n<div class=\"wrap\">\n  <h1>Which photo should lead the listing?</h1>\n  <p class=\"sub\">2-bed flat, Orchard Street. The first photo gets about 70% of clicks. Tap one.</p>\n  <div class=\"grid\" id=\"g\">\n    <button class=\"ph\" data-id=\"living_room\"><span class=\"badge\">Cover</span><span class=\"ai\">My pick</span>\n      <svg viewBox=\"0 0 400 300\" aria-hidden=\"true\"><defs><linearGradient id=\"a1\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#f3e7d3\"/><stop offset=\"1\" stop-color=\"#d9c6a6\"/></linearGradient></defs><rect width=\"400\" height=\"300\" fill=\"url(#a1)\"/><rect x=\"230\" y=\"40\" width=\"130\" height=\"150\" fill=\"#bfe0f2\"/><rect x=\"230\" y=\"40\" width=\"130\" height=\"150\" fill=\"none\" stroke=\"#fff\" stroke-width=\"8\"/><line x1=\"295\" y1=\"40\" x2=\"295\" y2=\"190\" stroke=\"#fff\" stroke-width=\"5\"/><rect y=\"230\" width=\"400\" height=\"70\" fill=\"#a77b52\"/><rect x=\"40\" y=\"170\" width=\"170\" height=\"60\" rx=\"14\" fill=\"#5d7f73\"/><rect x=\"40\" y=\"150\" width=\"170\" height=\"35\" rx=\"12\" fill=\"#6f9487\"/><circle cx=\"330\" cy=\"215\" r=\"18\" fill=\"#3f6b45\"/><rect x=\"322\" y=\"225\" width=\"16\" height=\"22\" fill=\"#8a5a3a\"/><rect x=\"90\" y=\"245\" width=\"120\" height=\"10\" rx=\"5\" fill=\"#8a5f3b\"/></svg>\n      <span class=\"cap\">Living room <small>bright, 4 pm</small></span></button>\n    <button class=\"ph\" data-id=\"kitchen\"><span class=\"badge\">Cover</span>\n      <svg viewBox=\"0 0 400 300\" aria-hidden=\"true\"><rect width=\"400\" height=\"300\" fill=\"#e9eef0\"/><rect y=\"200\" width=\"400\" height=\"100\" fill=\"#cfd8dc\"/><rect x=\"20\" y=\"40\" width=\"360\" height=\"60\" fill=\"#ffffff\" stroke=\"#b0bec5\" stroke-width=\"3\"/><g fill=\"#b0bec5\"><rect x=\"100\" y=\"40\" width=\"3\" height=\"60\"/><rect x=\"200\" y=\"40\" width=\"3\" height=\"60\"/><rect x=\"300\" y=\"40\" width=\"3\" height=\"60\"/></g><rect x=\"20\" y=\"150\" width=\"360\" height=\"12\" fill=\"#37474f\"/><rect x=\"20\" y=\"162\" width=\"360\" height=\"70\" fill=\"#ffffff\" stroke=\"#b0bec5\" stroke-width=\"3\"/><rect x=\"150\" y=\"120\" width=\"70\" height=\"30\" rx=\"4\" fill=\"#78909c\"/><circle cx=\"320\" cy=\"135\" r=\"14\" fill=\"#ef9a9a\"/><circle cx=\"60\" cy=\"132\" r=\"10\" fill=\"#a5d6a7\"/></svg>\n      <span class=\"cap\">Kitchen <small>new, 2025</small></span></button>\n    <button class=\"ph\" data-id=\"bedroom\"><span class=\"badge\">Cover</span>\n      <svg viewBox=\"0 0 400 300\" aria-hidden=\"true\"><rect width=\"400\" height=\"300\" fill=\"#dcd3e8\"/><rect y=\"240\" width=\"400\" height=\"60\" fill=\"#9c8569\"/><rect x=\"70\" y=\"140\" width=\"260\" height=\"90\" rx=\"10\" fill=\"#f5f5f5\"/><rect x=\"70\" y=\"120\" width=\"260\" height=\"40\" rx=\"10\" fill=\"#7e6ba3\"/><rect x=\"95\" y=\"128\" width=\"90\" height=\"28\" rx=\"10\" fill=\"#fff\"/><rect x=\"215\" y=\"128\" width=\"90\" height=\"28\" rx=\"10\" fill=\"#fff\"/><rect x=\"60\" y=\"220\" width=\"280\" height=\"20\" fill=\"#6d5a8c\"/><circle cx=\"355\" cy=\"80\" r=\"22\" fill=\"#fff4c2\"/></svg>\n      <span class=\"cap\">Bedroom <small>quiet side</small></span></button>\n    <button class=\"ph\" data-id=\"balcony\"><span class=\"badge\">Cover</span>\n      <svg viewBox=\"0 0 400 300\" aria-hidden=\"true\"><defs><linearGradient id=\"s1\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#ffb26b\"/><stop offset=\"1\" stop-color=\"#ffd9a0\"/></linearGradient></defs><rect width=\"400\" height=\"300\" fill=\"url(#s1)\"/><circle cx=\"290\" cy=\"150\" r=\"40\" fill=\"#fff1c1\"/><path d=\"M0 190 L60 150 L110 175 L170 130 L240 170 L300 140 L400 180 L400 300 L0 300Z\" fill=\"#7a6a8f\"/><rect y=\"215\" width=\"400\" height=\"85\" fill=\"#4b4453\"/><g stroke=\"#2d2833\" stroke-width=\"5\"><line x1=\"0\" y1=\"215\" x2=\"400\" y2=\"215\"/><line x1=\"40\" y1=\"215\" x2=\"40\" y2=\"300\"/><line x1=\"120\" y1=\"215\" x2=\"120\" y2=\"300\"/><line x1=\"200\" y1=\"215\" x2=\"200\" y2=\"300\"/><line x1=\"280\" y1=\"215\" x2=\"280\" y2=\"300\"/><line x1=\"360\" y1=\"215\" x2=\"360\" y2=\"300\"/></g></svg>\n      <span class=\"cap\">Balcony <small>sunset view</small></span></button>\n    <button class=\"ph\" data-id=\"bathroom\"><span class=\"badge\">Cover</span>\n      <svg viewBox=\"0 0 400 300\" aria-hidden=\"true\"><rect width=\"400\" height=\"300\" fill=\"#e0f2f1\"/><g stroke=\"#b2dfdb\" stroke-width=\"2\"><path d=\"M0 50H400M0 100H400M0 150H400M0 200H400M0 250H400M50 0V300M100 0V300M150 0V300M200 0V300M250 0V300M300 0V300M350 0V300\"/></g><rect x=\"60\" y=\"170\" width=\"200\" height=\"70\" rx=\"30\" fill=\"#fff\" stroke=\"#80cbc4\" stroke-width=\"4\"/><rect x=\"290\" y=\"80\" width=\"60\" height=\"80\" rx=\"8\" fill=\"#b3e5fc\" stroke=\"#fff\" stroke-width=\"5\"/></svg>\n      <span class=\"cap\">Bathroom <small>walk-in shower</small></span></button>\n    <button class=\"ph\" data-id=\"street\"><span class=\"badge\">Cover</span>\n      <svg viewBox=\"0 0 400 300\" aria-hidden=\"true\"><rect width=\"400\" height=\"300\" fill=\"#bcdcf0\"/><rect x=\"60\" y=\"60\" width=\"280\" height=\"240\" fill=\"#c98b6b\"/><g fill=\"#f4efe6\"><rect x=\"90\" y=\"90\" width=\"50\" height=\"60\"/><rect x=\"175\" y=\"90\" width=\"50\" height=\"60\"/><rect x=\"260\" y=\"90\" width=\"50\" height=\"60\"/><rect x=\"90\" y=\"180\" width=\"50\" height=\"60\"/><rect x=\"260\" y=\"180\" width=\"50\" height=\"60\"/></g><rect x=\"178\" y=\"200\" width=\"44\" height=\"100\" fill=\"#3e5c76\"/><circle cx=\"30\" cy=\"250\" r=\"30\" fill=\"#5f9e5a\"/><circle cx=\"372\" cy=\"250\" r=\"30\" fill=\"#5f9e5a\"/></svg>\n      <span class=\"cap\">Building <small>street view</small></span></button>\n  </div>\n</div>\n<div class=\"bar\"><div class=\"in\"><p id=\"s\">I'd lead with the living room: light sells.</p><button class=\"go\" id=\"go\" disabled>Use as cover</button></div></div>\n<script>\nvar pick=null,sent=false,names={};\ndocument.querySelectorAll('.ph').forEach(function(b){\n  names[b.dataset.id]=b.querySelector('.cap').firstChild.textContent.trim();\n  b.onclick=function(){document.querySelectorAll('.ph').forEach(function(x){x.classList.remove('on')});b.classList.add('on');pick=b.dataset.id;document.getElementById('go').disabled=false;document.getElementById('s').textContent=names[pick]+' as the cover photo';};\n});\ndocument.getElementById('go').onclick=function(){\n  if(sent||!pick)return;sent=true;\n  var order=[pick].concat(Object.keys(names).filter(function(k){return k!==pick}));\n  (window.Bridgit||window.Bridget).respond(JSON.stringify({cover:pick,agreed_with_suggestion:pick==='living_room',order:order}));\n};\n</script>\n"
}
Raw JSONhtmlimagespicker
Your agent
Reply to Anna about the venue?

Anna asked if Friday still works for the venue visit.

My draft:
“Hi Anna, Friday at 3 works. I’ll bring the floor plans and the catering quote. See you at the front desk. Best, Alex”

QuestionWork & paperwork

Reply to Anna about the venue?

Show a draft, let them send it, or type changes. Text replies come back in values.

Owner sees
The draft reply, a Changes field, Send as is and Send with my changes.
Then
send: send the draft. revise: rewrite with the changes and ask again.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "Reply to Anna about the venue?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "revise",
    "values": {
      "changes": "Say Thursday works too, and drop the last line"
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.6 KB
{
  "kind": "question",
  "title": "Reply to Anna about the venue?",
  "text": "Anna asked if Friday still works for the venue visit.\n\nMy draft:\n“Hi Anna, Friday at 3 works. I’ll bring the floor plans and the catering quote. See you at the front desk. Best, Alex”",
  "controls": [
    {
      "type": "text",
      "id": "changes",
      "label": "Changes (optional)",
      "placeholder": "e.g. make it shorter"
    },
    {
      "type": "button",
      "id": "send",
      "label": "Send as is",
      "style": "primary"
    },
    {
      "type": "button",
      "id": "revise",
      "label": "Send with my changes"
    }
  ]
}
Raw JSONtextbuttonsemail
Your agentUrgent
Is this you signing in to GitHub?

Chrome on Windows, Warsaw, Poland. Just now (21:03).

If you don’t answer in 5 minutes I’ll block it.

QuestionWork & paperwork

Is this you signing in to GitHub?

Confirm a sign-in from a new device. Short expiry; no answer means no.

Owner sees
An urgent card with the device and place, and Yes, that’s me / No, block it.
Then
allow: let the sign-in through. block or expired: deny it and suggest changing the password.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "question",
  "status": "answered",
  "title": "Is this you signing in to GitHub?",
  "priority": "urgent",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": "block",
    "values": {}
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 0.5 KB
{
  "kind": "question",
  "title": "Is this you signing in to GitHub?",
  "text": "Chrome on Windows, Warsaw, Poland. Just now (21:03).\n\nIf you don’t answer in 5 minutes I’ll block it.",
  "priority": "urgent",
  "expires_in_seconds": 300,
  "controls": [
    {
      "type": "button",
      "id": "allow",
      "label": "Yes, that’s me",
      "style": "primary"
    },
    {
      "type": "button",
      "id": "block",
      "label": "No, block it"
    }
  ]
}
Raw JSONurgentsecuritybuttons
Your agentDone
Release 1.7.0 build started 14:02
✓ Fetched main @ 33a2de9
✓ Unit tests: 1,284 passed (2 min 11 s)
… Building release APK
✓ APK signed, 18.4 MB
✓ Uploaded to the beta channel

Done at 14:09. Your 3 testers get it within the hour.
Text panelWork & paperwork

Release 1.7.0: build and upload

A live text panel the agent streams into while it works. The owner watches progress from across the room.

Owner sees
A scrolling panel that fills in line by line until the agent marks it done.
Then
POST /v1/text/stream {"interaction_id","append"} for each line, then {"interaction_id","done":true}.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "text",
  "status": "seen",
  "title": "Release 1.7.0: build and upload",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": null,
  "answered_at": null,
  "closed_at": null,
  "length": 230,
  "done": true,
  "request_id": "5f0c2a9e41b7"
}
The stream calls
POST /v1/text/stream {"interaction_id":42,"append":"Release 1.7.0 build started 14:02\n"}
POST /v1/text/stream {"interaction_id":42,"append":"✓ Fetched main @ 33a2de9\n"}
POST /v1/text/stream {"interaction_id":42,"append":"✓ Unit tests: 1,284 passed (2 min 11 s)\n"}
POST /v1/text/stream {"interaction_id":42,"append":"… Building release APK\n"}
POST /v1/text/stream {"interaction_id":42,"append":"✓ APK signed, 18.4 MB\n"}
POST /v1/text/stream {"interaction_id":42,"append":"✓ Uploaded to the beta channel\n"}
POST /v1/text/stream {"interaction_id":42,"append":"\nDone at 14:09. Your 3 testers get it within the hour.\n"}
POST /v1/text/stream {"interaction_id":42,"done":true}
The JSON to send 0.1 KB
{
  "kind": "text",
  "title": "Release 1.7.0: build and upload"
}
Raw JSONtextlivestream
HTML cardWork & paperwork

5 action items from the Q1 planning call

Turn a meeting into a checked, assigned to-do list: accept each item, cycle its owner and due date with one tap, then send it to the team.

Owner sees
The meeting header with attendee avatars and five action items, each with an accept tick, an owner pill and a due-date chip, plus a live "for you / for others" count.
Then
send: post the accepted items to the team chat with owners and dates, add the owner's own items to their task list and calendar, and drop the ones left unticked. later: keep the draft and remind them tomorrow morning.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "5 action items from the Q1 planning call",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "action": "send",
      "items": [
        {
          "id": "a1",
          "task": "Draft the Q1 budget for the Kraków office",
          "accepted": true,
          "owner": "you",
          "due": "2026-11-06"
        },
        {
          "id": "a2",
          "task": "Book the venue for the January offsite",
          "accepted": true,
          "owner": "maja",
          "due": "2026-11-13"
        },
        {
          "id": "a3",
          "task": "Share the hiring plan with finance",
          "accepted": true,
          "owner": "ines",
          "due": "2026-11-09"
        },
        {
          "id": "a4",
          "task": "Update the product roadmap slides",
          "accepted": false,
          "owner": "tomasz",
          "due": "2026-11-13"
        },
        {
          "id": "a5",
          "task": "Send meeting notes to the Lisbon team",
          "accepted": true,
          "owner": "you",
          "due": "2026-11-06"
        }
      ]
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 8.5 KB
{
  "kind": "html",
  "title": "5 action items from the Q1 planning call",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#f4f6f8;color:#1e293b;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:13px;font-weight:600;color:#0f766e;letter-spacing:.04em;text-transform:uppercase;margin:0 0 6px}\nh1{font-size:22px;line-height:1.25;margin:0 0 10px;letter-spacing:-.01em}\n.meta{display:flex;align-items:center;gap:10px;color:#475569;font-size:14px;margin-bottom:18px;flex-wrap:wrap}\n.avs{display:flex}\n.av{width:32px;height:32px;border-radius:50%;display:grid;place-content:center;font-size:11px;font-weight:700;border:2px solid #f4f6f8;margin-left:-6px;color:#1e293b}\n.av:first-child{margin-left:0}\n.count{display:flex;gap:8px;margin-bottom:12px;font-size:14px;font-variant-numeric:tabular-nums}\n.count span{background:#fff;border-radius:999px;padding:6px 12px;box-shadow:0 1px 2px rgba(15,23,42,.06);color:#475569}\n.count b{color:#1e293b}\n.count .you b{color:#0f766e}\n.list{display:flex;flex-direction:column;gap:10px;margin-bottom:20px}\n.item{background:#fff;border-radius:16px;padding:14px 14px 12px;display:grid;grid-template-columns:48px 1fr;gap:4px 10px;box-shadow:0 1px 3px rgba(15,23,42,.06),0 4px 14px rgba(15,23,42,.04);border:1.5px solid transparent;transition:border-color .18s,background .18s,opacity .18s}\n.item.mine{border-color:#99e0dc;background:#f0fbfa}\n.item.off{opacity:.55}\n.tick{grid-row:1/3;width:48px;height:48px;border:0;background:none;padding:0;cursor:pointer;display:grid;place-content:center}\n.tick i{width:26px;height:26px;border-radius:8px;border:2px solid #cbd5e1;display:grid;place-content:center;transition:all .18s;color:#fff;font-style:normal;font-size:15px;font-weight:800}\n.tick.on i{background:#0ea5a4;border-color:#0ea5a4}\n.task{font-size:15.5px;font-weight:550;padding-top:2px}\n.item.off .task{text-decoration:line-through}\n.chips{display:flex;gap:8px;flex-wrap:wrap}\n.chip{font:inherit;font-size:13.5px;font-weight:600;border:1px solid #e2e8f0;background:#f8fafc;color:#334155;border-radius:999px;padding:0 14px;min-height:48px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background .15s,border-color .15s,transform .1s}\n.chip.own.me{background:#0f766e;border-color:#0f766e;color:#fff}\n.chip small{font-size:12px;opacity:.75;font-weight:500}\n.chip:active,.main:active{transform:scale(.98)}\n.main{width:100%;font:inherit;font-weight:700;border:0;border-radius:14px;min-height:54px;background:#0f766e;color:#fff;cursor:pointer;box-shadow:0 6px 18px rgba(14,165,164,.28);transition:background .15s,transform .1s}\n.main:disabled{background:#94a3b8;box-shadow:none;cursor:default}\n.alt{width:100%;font:inherit;font-weight:600;border:0;background:none;color:#475569;min-height:48px;margin-top:6px;cursor:pointer}\n.status{text-align:center;color:#475569;font-size:14px;min-height:20px;margin-top:8px}\n.done{color:#0f766e}\n</style>\n<div class=\"wrap\">\n  <p class=\"eyebrow\" id=\"eyebrow\"></p>\n  <h1 id=\"title\"></h1>\n  <div class=\"meta\"><div class=\"avs\" id=\"avs\"></div><span id=\"when\"></span></div>\n  <div class=\"count\"><span class=\"you\"><b id=\"nYou\">0</b> for you</span><span><b id=\"nOther\">0</b> for others</span></div>\n  <div class=\"list\" id=\"list\"></div>\n  <button class=\"main\" id=\"send\"></button>\n  <button class=\"alt\" id=\"later\"></button>\n  <div class=\"status\" id=\"status\"></div>\n</div>\n<script>\n// answer.data: {action:\"send\"|\"later\", items:[{id, task, accepted, owner:\"you\"|\"maja\"|…, due:\"2026-11-06\"|null}]}\n// The card's content. Change only this object.\nvar CARD = {\n  eyebrow: 'Action items',\n  title: 'Q1 planning call',\n  when: 'Tue 3 Nov · 45 min · 6 people',\n  attendees: [\n    {id:'you', name:'You', initials:'Me', colour:'#99f6e4'},\n    {id:'maja', name:'Maja', initials:'MK', colour:'#fde68a'},\n    {id:'tomasz', name:'Tomasz', initials:'TW', colour:'#c7d2fe'},\n    {id:'ines', name:'Ines', initials:'IR', colour:'#fbcfe8'},\n    {id:'piotr', initials:'PN', colour:'#bbf7d0'},\n    {id:'lea', initials:'LB', colour:'#fed7aa'}\n  ],\n  owners: ['you','maja','tomasz','ines'],\n  dues: [\n    {label:'Fri', date:'2026-11-06'},\n    {label:'Next Mon', date:'2026-11-09'},\n    {label:'Next Fri', date:'2026-11-13'},\n    {label:'No date', date:null}\n  ],\n  items: [\n    {id:'a1', task:'Draft the Q1 budget for the Kraków office', owner:'you', due:0},\n    {id:'a2', task:'Book the venue for the January offsite', owner:'maja', due:2},\n    {id:'a3', task:'Share the hiring plan with finance', owner:'you', due:1},\n    {id:'a4', task:'Update the product roadmap slides', owner:'tomasz', due:2},\n    {id:'a5', task:'Send meeting notes to the Lisbon team', owner:'you', due:0}\n  ],\n  sendLabel: 'Send to the team',\n  laterLabel: 'Edit later',\n  sending: 'Sending…',\n  doneText: {send:'Sent to the team', later:'Saved, edit them later'},\n  preview: 'Preview only: open this card in Bridgit to answer.'\n};\nvar $ = function(id){return document.getElementById(id)};\nvar sent = false, rows = [], last = 'send';\nvar people = {}; CARD.attendees.forEach(function(a){people[a.id]=a});\n$('eyebrow').textContent = CARD.eyebrow;\n$('title').textContent = CARD.title;\n$('when').textContent = CARD.when;\n$('later').textContent = CARD.laterLabel;\nCARD.attendees.forEach(function(a){var d=document.createElement('div');d.className='av';d.style.background=a.colour;d.textContent=a.initials;$('avs').appendChild(d)});\nCARD.items.forEach(function(it){\n  var st = {id:it.id, task:it.task, accepted:true, owner:CARD.owners.indexOf(it.owner), due:it.due};\n  var row = document.createElement('div'); row.className='item';\n  var tick = document.createElement('button'); tick.className='tick on'; tick.setAttribute('aria-label','Accept');\n  var box = document.createElement('i'); box.textContent='✓'; tick.appendChild(box);\n  var task = document.createElement('div'); task.className='task'; task.textContent=it.task;\n  var chips = document.createElement('div'); chips.className='chips';\n  var own = document.createElement('button'); own.className='chip own';\n  var due = document.createElement('button'); due.className='chip';\n  chips.appendChild(own); chips.appendChild(due);\n  row.appendChild(tick); row.appendChild(task); row.appendChild(chips);\n  $('list').appendChild(row);\n  st.el = {row:row, tick:tick, box:box, own:own, due:due};\n  tick.onclick = function(){if(sent)return;st.accepted=!st.accepted;draw()};\n  own.onclick = function(){if(sent)return;st.owner=(st.owner+1)%CARD.owners.length;draw()};\n  due.onclick = function(){if(sent)return;st.due=(st.due+1)%CARD.dues.length;draw()};\n  rows.push(st);\n});\nfunction draw(){\n  var you=0, other=0;\n  rows.forEach(function(st){\n    var oid = CARD.owners[st.owner], mine = oid==='you';\n    st.el.row.classList.toggle('mine', mine && st.accepted);\n    st.el.row.classList.toggle('off', !st.accepted);\n    st.el.tick.classList.toggle('on', st.accepted);\n    st.el.box.textContent = st.accepted ? '✓' : '';\n    st.el.own.classList.toggle('me', mine);\n    st.el.own.textContent = people[oid].name;\n    st.el.due.textContent = '';\n    var s = document.createElement('small'); s.textContent='Due'; st.el.due.appendChild(s);\n    st.el.due.appendChild(document.createTextNode(CARD.dues[st.due].label));\n    if (st.accepted) { if (mine) you++; else other++; }\n  });\n  $('nYou').textContent = you; $('nOther').textContent = other;\n  var n = you + other;\n  $('send').disabled = n === 0;\n  $('send').textContent = n ? CARD.sendLabel + ' · ' + n + (n === 1 ? ' item' : ' items') : 'Accept at least one item';\n}\ndraw();\nfunction answer(action){\n  if (sent) return;\n  var b = window.Bridgit || window.Bridget;\n  if (!b) { $('status').textContent = CARD.preview; return; }\n  sent = true; last = action;\n  document.querySelectorAll('button').forEach(function(x){x.disabled=true});\n  $('status').textContent = CARD.sending;\n  b.respond(JSON.stringify({action:action, items:rows.map(function(st){return {id:st.id, task:st.task, accepted:st.accepted, owner:CARD.owners[st.owner], due:CARD.dues[st.due].date}})}));\n}\n$('send').onclick = function(){answer('send')};\n$('later').onclick = function(){answer('later')};\ndocument.addEventListener('bridgit:answered', function(e){\n  e.preventDefault();\n  $('title').textContent = '✓ ' + CARD.doneText[last];\n  $('title').className = 'done';\n  $('status').textContent = '';\n  setTimeout(function(){$('title').scrollIntoView({block:'center'})}, 400);\n});\n</script>\n",
  "expires_in_seconds": 86400
}
Raw JSONhtmltasksmeetingsassign
HTML cardWork & paperwork

Which logo for Fern & Pine Studio?

A design review in one card: compare three logo concepts side by side, flip them onto a dark background, pick one and say why, so the designer gets clear feedback.

Owner sees
Three large logo tiles drawn in the card, a "Show on dark" switch, reason chips and a short note box.
Then
A, B or C: send the pick, the reasons and the note to the designer and ask for final files. pick null (None of these): ask the designer for a new round, passing on the note.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Which logo for Fern & Pine Studio?",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "pick": "A",
      "reasons": [
        "simple",
        "works_small"
      ],
      "note": "Make the leaf a touch thicker",
      "checked_dark": true
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 9.9 KB
{
  "kind": "html",
  "title": "Which logo for Fern & Pine Studio?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#f6f4f0;color:#151515;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#6b6358;margin:0 0 6px}\nh1{font-size:24px;line-height:1.2;margin:0 0 6px;letter-spacing:-.02em;font-weight:700}\n.sub{color:#5c554c;font-size:15px;margin:0 0 16px}\n.bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}\n.bar span{font-size:14px;color:#5c554c}\n.sw{font:inherit;font-size:14px;font-weight:600;border:1px solid #e3ddd2;background:#fff;color:#151515;border-radius:999px;min-height:48px;padding:0 14px 0 8px;display:inline-flex;align-items:center;gap:10px;cursor:pointer}\n.sw i{width:40px;height:24px;border-radius:999px;background:#d9d2c5;position:relative;transition:background .18s}\n.sw i:after{content:\"\";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .18s}\n.sw.on i{background:#151515}.sw.on i:after{transform:translateX(16px)}\n.tiles{display:flex;flex-direction:column;gap:12px;margin-bottom:22px}\n.tile{position:relative;font:inherit;width:100%;border:2px solid #e8e2d7;background:#fff;border-radius:18px;padding:22px 16px 14px;cursor:pointer;text-align:left;color:#151515;box-shadow:0 2px 10px rgba(60,45,20,.05);transition:border-color .18s,background .18s,transform .1s,box-shadow .18s}\n.tile:active,.chip:active,.main:active{transform:scale(.98)}\n.tile.on{border-color:#151515;box-shadow:0 8px 24px rgba(21,21,21,.12)}\n.tile svg{display:block;width:100%;height:96px}\n.tile .lab{display:block;margin-top:10px;font-size:13px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#6b6358}\n.tile .ok{position:absolute;top:12px;right:12px;width:28px;height:28px;border-radius:50%;background:#151515;color:#fff;display:grid;place-content:center;font-size:15px;font-weight:800;opacity:0;transform:scale(.6);transition:all .18s}\n.tile.on .ok{opacity:1;transform:none}\n.dark .tile{background:#1b1b1b;border-color:#2e2e2e}\n.dark .tile.on{border-color:#f6f4f0}\n.dark .tile .lab{color:#b8b0a3}\n.dark .tile .ok{background:#f6f4f0;color:#151515}\n.ink{fill:#151515}.dark .ink{fill:#f6f4f0}\n.fern{fill:#1f5c3a}.dark .fern{fill:#7fc79a}\n.terra{fill:#b4532a}.dark .terra{fill:#e08a5f}\n.wordc{fill:#fff}\nh2{font-size:15px;margin:0 0 10px}\nh2 small{font-weight:400;color:#6b6358}\n.chips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}\n.chip{font:inherit;font-size:14.5px;font-weight:600;border:1px solid #e3ddd2;background:#fff;color:#151515;border-radius:999px;min-height:48px;padding:0 16px;cursor:pointer;transition:background .15s,color .15s,border-color .15s,transform .1s}\n.chip.on{background:#151515;border-color:#151515;color:#fff}\ntextarea{width:100%;font:inherit;font-size:15px;border:1px solid #e3ddd2;background:#fff;color:#151515;border-radius:14px;padding:12px 14px;min-height:84px;resize:vertical;margin-bottom:20px}\ntextarea:focus{outline:2px solid #151515;outline-offset:1px}\n.main{width:100%;font:inherit;font-weight:700;border:0;border-radius:14px;min-height:54px;background:#151515;color:#fff;cursor:pointer;transition:background .15s,transform .1s}\n.main:disabled{background:#cfc8bb;color:#5c554c;cursor:default}\n.alt{width:100%;font:inherit;font-weight:600;border:0;background:none;color:#5c554c;min-height:48px;margin-top:6px;cursor:pointer}\n.status{text-align:center;color:#5c554c;font-size:14px;min-height:20px;margin-top:8px}\n</style>\n<div class=\"wrap\">\n  <p class=\"eyebrow\" id=\"eyebrow\"></p>\n  <h1 id=\"title\"></h1>\n  <p class=\"sub\" id=\"sub\"></p>\n  <div class=\"bar\"><span id=\"hint\"></span><button class=\"sw\" id=\"dark\"><i></i><span id=\"darkLabel\"></span></button></div>\n  <div class=\"tiles\" id=\"tiles\">\n    <button class=\"tile\" data-id=\"A\"><span class=\"ok\">✓</span>\n      <svg viewBox=\"0 0 300 96\" aria-hidden=\"true\"><circle cx=\"48\" cy=\"48\" r=\"38\" class=\"fern\"/><path d=\"M48 80 V22\" stroke=\"#fff\" stroke-width=\"3\" stroke-linecap=\"round\"/><path d=\"M48 30 q-14 0 -20 8 q12 2 20 -2zM48 30 q14 0 20 8 q-12 2 -20 -2zM48 44 q-17 0 -24 10 q15 2 24 -3zM48 44 q17 0 24 10 q-15 2 -24 -3zM48 58 q-14 0 -20 9 q12 1 20 -3zM48 58 q14 0 20 9 q-12 1 -20 -3z\" fill=\"#fff\"/><text data-k=\"name\" x=\"102\" y=\"45\" class=\"fern\" font-family=\"Georgia,serif\" font-size=\"26\" font-weight=\"700\"></text><text data-k=\"tag\" x=\"103\" y=\"68\" class=\"fern\" font-family=\"system-ui,sans-serif\" font-size=\"12\" letter-spacing=\"4\"></text></svg>\n      <span class=\"lab\"></span></button>\n    <button class=\"tile\" data-id=\"B\"><span class=\"ok\">✓</span>\n      <svg viewBox=\"0 0 300 96\" aria-hidden=\"true\"><path d=\"M48 8 L70 40 H26Z M48 26 L76 62 H20Z M48 46 L82 84 H14Z\" class=\"ink\"/><text data-k=\"caps\" x=\"102\" y=\"52\" class=\"ink\" font-family=\"system-ui,sans-serif\" font-size=\"24\" font-weight=\"800\" letter-spacing=\"2\"></text><text data-k=\"tag\" x=\"103\" y=\"72\" class=\"ink\" font-family=\"system-ui,sans-serif\" font-size=\"11\" letter-spacing=\"6\"></text></svg>\n      <span class=\"lab\"></span></button>\n    <button class=\"tile\" data-id=\"C\"><span class=\"ok\">✓</span>\n      <svg viewBox=\"0 0 300 96\" aria-hidden=\"true\"><rect x=\"10\" y=\"10\" width=\"76\" height=\"76\" rx=\"20\" class=\"terra\"/><text data-k=\"mono\" x=\"48\" y=\"59\" text-anchor=\"middle\" class=\"wordc\" font-family=\"Georgia,serif\" font-size=\"28\" font-style=\"italic\" font-weight=\"700\"></text><text data-k=\"lower\" x=\"102\" y=\"47\" class=\"terra\" font-family=\"system-ui,sans-serif\" font-size=\"23\" font-weight=\"600\"></text><text data-k=\"tagC\" x=\"103\" y=\"68\" class=\"terra\" font-family=\"Georgia,serif\" font-size=\"14\" font-style=\"italic\"></text></svg>\n      <span class=\"lab\"></span></button>\n  </div>\n  <h2 id=\"whyTitle\"></h2>\n  <div class=\"chips\" id=\"chips\"></div>\n  <textarea id=\"note\" maxlength=\"300\" rows=\"3\"></textarea>\n  <button class=\"main\" id=\"send\" disabled></button>\n  <button class=\"alt\" id=\"none\"></button>\n  <div class=\"status\" id=\"status\"></div>\n</div>\n<script>\n// answer.data: {pick:\"A\"|\"B\"|\"C\"|null, reasons:[\"simple\",…], note:\"…\", checked_dark:true|false}\n// The card's content. Change only this object.\nvar CARD = {\n  eyebrow: 'Fern & Pine Studio · Logo round 2',\n  title: 'Which logo should we go with?',\n  sub: 'Three concepts from Ola. Tap the one you like best.',\n  wordmark: {name:'Fern & Pine', caps:'FERN+PINE', lower:'fern & pine', mono:'F&P', tag:'STUDIO', tagC:'design studio'},\n  hint: 'Tap to choose',\n  darkLabel: 'Show on dark',\n  logos: [\n    {id:'A', label:'A · Leaf'},\n    {id:'B', label:'B · Pines'},\n    {id:'C', label:'C · Monogram'}\n  ],\n  whyTitle: 'Why this one?',\n  whyNote: 'pick any',\n  reasons: [\n    {id:'simple', label:'Simple'},\n    {id:'warm', label:'Feels warm'},\n    {id:'works_small', label:'Works small'},\n    {id:'memorable', label:'Memorable'}\n  ],\n  notePlaceholder: 'Anything to change? (optional)',\n  pickFirst: 'Tap a logo first',\n  choose: 'Choose ',\n  noneLabel: 'None of these, try again',\n  sending: 'Sending…',\n  doneText: {pick:'Sent: you picked ', none:'Sent: asking for new ideas'},\n  preview: 'Preview only: open this card in Bridgit to answer.'\n};\nvar $ = function(id){return document.getElementById(id)};\nvar pick = null, reasons = {}, dark = false, checkedDark = false, sent = false;\n['eyebrow','title','sub','hint','darkLabel','whyTitle'].forEach(function(k){$(k).textContent = CARD[k]});\nvar sm = document.createElement('small'); sm.textContent = ' · ' + CARD.whyNote; $('whyTitle').appendChild(sm);\ndocument.querySelectorAll('text[data-k]').forEach(function(t){ t.textContent = CARD.wordmark[t.getAttribute('data-k')] });\n$('note').placeholder = CARD.notePlaceholder;\n$('none').textContent = CARD.noneLabel;\nvar tiles = document.querySelectorAll('.tile');\ntiles.forEach(function(t, i){\n  t.querySelector('.lab').textContent = CARD.logos[i].label;\n  t.onclick = function(){ if (sent) return; pick = t.dataset.id; draw(); };\n});\nCARD.reasons.forEach(function(r){\n  var b = document.createElement('button'); b.className = 'chip'; b.textContent = r.label;\n  b.onclick = function(){ if (sent) return; reasons[r.id] = !reasons[r.id]; b.classList.toggle('on', reasons[r.id]); };\n  $('chips').appendChild(b);\n});\n$('dark').onclick = function(){\n  if (sent) return; dark = !dark; if (dark) checkedDark = true;\n  $('dark').classList.toggle('on', dark); $('tiles').classList.toggle('dark', dark);\n};\nfunction draw(){\n  tiles.forEach(function(t){ t.classList.toggle('on', t.dataset.id === pick) });\n  $('send').disabled = !pick;\n  $('send').textContent = pick ? CARD.choose + pick : CARD.pickFirst;\n}\ndraw();\nvar last = 'pick';\nfunction answer(kind){\n  if (sent) return;\n  var b = window.Bridgit || window.Bridget;\n  if (!b) { $('status').textContent = CARD.preview; return; }\n  sent = true; last = kind;\n  document.querySelectorAll('button,textarea').forEach(function(x){ x.disabled = true });\n  $('status').textContent = CARD.sending;\n  b.respond(JSON.stringify({\n    pick: kind === 'pick' ? pick : null,\n    reasons: CARD.reasons.filter(function(r){ return reasons[r.id] }).map(function(r){ return r.id }),\n    note: $('note').value.trim(),\n    checked_dark: checkedDark\n  }));\n}\n$('send').onclick = function(){ answer('pick') };\n$('none').onclick = function(){ answer('none') };\ndocument.addEventListener('bridgit:answered', function(e){\n  e.preventDefault();\n  $('title').textContent = '✓ ' + CARD.doneText[last] + (last === 'pick' ? pick : '');\n  $('status').textContent = '';\n  setTimeout(function(){ $('title').scrollIntoView({block:'center'}) }, 400);\n});\n</script>\n",
  "expires_in_seconds": 259200
}
Raw JSONhtmldesignchoicefeedback

Family

HTML cardFamily

Animal & planet quiz for Zosia

A five-question picture quiz with big colourful buttons, cheers and a star score.

Owner sees
One emoji question at a time, four big answer buttons, a celebration and the score.
Then
Say well done out loud (POST /v1/speaker/say) and offer a harder round.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Animal & planet quiz for Zosia",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "score": 4,
      "out_of": 5,
      "seconds": 48,
      "answers": [
        {
          "question": 1,
          "picked": "8",
          "ok": true
        },
        {
          "question": 2,
          "picked": "Earth",
          "ok": true
        },
        {
          "question": 3,
          "picked": "Giraffe",
          "ok": true
        },
        {
          "question": 4,
          "picked": "6",
          "ok": false
        },
        {
          "question": 5,
          "picked": "Honey",
          "ok": true
        }
      ]
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 3.7 KB
{
  "kind": "html",
  "title": "Animal & planet quiz for Zosia",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#fff4d6;color:#2b2140;font:18px/1.4 \"Comic Sans MS\",\"Chalkboard SE\",system-ui,-apple-system,Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:620px;margin:0 auto;padding:20px 16px 30px;text-align:center}\n.dots{display:flex;justify-content:center;gap:8px;margin-bottom:14px}\n.dots i{width:14px;height:14px;border-radius:50%;background:#f1d79a}\n.dots i.ok{background:#3bb273}.dots i.no{background:#ff7a59}.dots i.now{outline:3px solid #7b5cff;outline-offset:2px}\n.big{font-size:84px;line-height:1;margin:10px 0}\nh1{font-size:26px;margin:6px 0 16px}\n.ans{display:grid;grid-template-columns:1fr 1fr;gap:12px}\n.ans button{font:inherit;font-size:22px;font-weight:700;border:0;border-radius:20px;padding:18px 10px;min-height:80px;cursor:pointer;color:#fff;box-shadow:0 5px 0 rgba(0,0,0,.18);transition:transform .1s}\n.ans button:active{transform:translateY(3px);box-shadow:0 2px 0 rgba(0,0,0,.18)}\n.ans button:nth-child(1){background:#7b5cff}.ans button:nth-child(2){background:#ff8a3d}.ans button:nth-child(3){background:#1fb5c9}.ans button:nth-child(4){background:#e8478b}\n.ans button.right{background:#3bb273!important}.ans button.wrong{opacity:.35}\n.msg{min-height:30px;margin:14px 0 0;font-weight:700}\n.end .stars{font-size:56px;letter-spacing:6px}\n.end button{font:inherit;font-weight:700;border:0;border-radius:20px;padding:16px 28px;background:#7b5cff;color:#fff;font-size:20px;margin-top:16px;cursor:pointer}\n</style>\n<div class=\"wrap\" id=\"app\"></div>\n<script>\nvar Q=[\n {e:'🐙',q:'How many arms does an octopus have?',a:['6','8','10','12'],r:1},\n {e:'🌍',q:'Which planet do we live on?',a:['Mars','Venus','Earth','Jupiter'],r:2},\n {e:'🦒',q:'Which animal has the longest neck?',a:['Giraffe','Horse','Camel','Ostrich'],r:0},\n {e:'🌈',q:'How many colours are in a rainbow?',a:['5','6','7','9'],r:2},\n {e:'🐝',q:'What do bees make?',a:['Milk','Honey','Jam','Butter'],r:1}\n],i=0,score=0,picks=[],start=Date.now(),sent=false,app=document.getElementById('app');\nvar cheers=['Brilliant!','You got it!','Super smart!','Yes!!','Amazing!'];\nfunction dots(){return '<div class=\"dots\">'+Q.map(function(_,k){return '<i class=\"'+(k<picks.length?(picks[k].ok?'ok':'no'):(k===i?'now':''))+'\"></i>'}).join('')+'</div>'}\nfunction show(){\n  if(i>=Q.length)return done();\n  var q=Q[i];\n  app.innerHTML=dots()+'<div class=\"big\">'+q.e+'</div><h1>'+q.q+'</h1><div class=\"ans\">'+q.a.map(function(t,k){return '<button data-k=\"'+k+'\">'+t+'</button>'}).join('')+'</div><p class=\"msg\" id=\"m\"></p>';\n  app.querySelectorAll('.ans button').forEach(function(b){b.onclick=function(){pick(+b.dataset.k)}});\n}\nfunction pick(k){\n  var q=Q[i],ok=k===q.r;if(picks.length>i)return;\n  picks.push({question:i+1,picked:q.a[k],ok:ok});if(ok)score++;\n  app.querySelectorAll('.ans button').forEach(function(b,n){b.disabled=true;b.classList.add(n===q.r?'right':'wrong')});\n  document.getElementById('m').textContent=ok?cheers[i%cheers.length]+' 🎉':'Nice try! It’s '+q.a[q.r]+'.';\n  setTimeout(function(){i++;show()},1300);\n}\nfunction done(){\n  var stars='⭐'.repeat(score)+'☆'.repeat(Q.length-score);\n  app.innerHTML=dots()+'<div class=\"end\"><div class=\"big\">'+(score>=4?'🏆':score>=2?'🎈':'🌟')+'</div><h1>You got '+score+' out of '+Q.length+'!</h1><div class=\"stars\">'+stars+'</div><button id=\"fin\">Tell Dad my score</button></div>';\n  document.getElementById('fin').onclick=function(){if(sent)return;sent=true;(window.Bridgit||window.Bridget).respond(JSON.stringify({score:score,out_of:Q.length,seconds:Math.round((Date.now()-start)/1000),answers:picks}))};\n}\nshow();\n</script>\n"
}
Raw JSONhtmlgamekids
Your agentLive
Pancakes for 4 (12 pancakes)

You need: 250 g flour, 2 eggs, 500 ml milk, 1 tbsp sugar, pinch of salt, butter for the pan.

Step 1. Whisk flour, sugar and salt in a big bowl.
Step 2. Make a well, crack in the eggs, add half the milk. Whisk smooth.
Step 3. Whisk in the rest of the milk. Rest 10 minutes. (I’ll set a timer.)
Text panelFamily

Pancakes for 4

A hands-free recipe on a propped-up tablet: the agent streams the next step when asked.

Owner sees
A big text panel with the ingredients, then each step as the agent adds it.
Then
Stream the next step when the owner says "next" (wake word event), done:true after the last step.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "text",
  "status": "seen",
  "title": "Pancakes for 4",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": null,
  "answered_at": null,
  "closed_at": null,
  "length": 324,
  "done": false,
  "request_id": "5f0c2a9e41b7"
}
The stream calls
POST /v1/text/stream {"interaction_id":42,"append":"Pancakes for 4 (12 pancakes)\n\n"}
POST /v1/text/stream {"interaction_id":42,"append":"You need: 250 g flour, 2 eggs, 500 ml milk, 1 tbsp sugar, pinch of salt, butter for the pan.\n\n"}
POST /v1/text/stream {"interaction_id":42,"append":"Step 1. Whisk flour, sugar and salt in a big bowl.\n"}
POST /v1/text/stream {"interaction_id":42,"append":"Step 2. Make a well, crack in the eggs, add half the milk. Whisk smooth.\n"}
POST /v1/text/stream {"interaction_id":42,"append":"Step 3. Whisk in the rest of the milk. Rest 10 minutes. (I’ll set a timer.)\n"}
POST /v1/text/stream {"interaction_id":42,"append":"…"}
The JSON to send 0.0 KB
{
  "kind": "text",
  "title": "Pancakes for 4"
}
Raw JSONtextlivehands-free
HTML cardFamily

Bedtime story for Zosia: you choose!

A playful picker for a child: big emoji tiles to choose a hero, a place and a twist, with a live sentence that builds the story as they tap.

Owner sees
A night-sky card with three rows of big emoji tiles, a Short / Medium / Long switch, a live story sentence and a yellow "Tell my story ✨" button.
Then
Write and read aloud a story of about that many minutes using the chosen hero, place and twist, then send a goodnight notice. surprise true: mention that the story was picked by chance.
Agent gets back GET /v1/interactions/42?wait=55
{
  "interaction_id": 42,
  "kind": "html",
  "status": "answered",
  "title": "Bedtime story for Zosia: you choose!",
  "priority": "normal",
  "created_at": "2026-11-01T18:00:00+01:00",
  "expires_at": "2026-11-02T18:00:00+01:00",
  "seen_at": "2026-11-01T18:00:04+01:00",
  "answer": {
    "button": null,
    "values": {},
    "data": {
      "hero": "fox",
      "place": "volcano",
      "twist": "key",
      "minutes": 6,
      "surprise": false
    }
  },
  "answered_at": "2026-11-01T18:00:21+01:00",
  "closed_at": "2026-11-01T18:00:21+01:00",
  "request_id": "5f0c2a9e41b7"
}
The JSON to send 9.3 KB
{
  "kind": "html",
  "title": "Bedtime story for Zosia: you choose!",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:linear-gradient(170deg,#2b1b5a 0%,#45288a 55%,#6a3fb5 100%);color:#fff;font:17px/1.4 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{position:relative;max-width:520px;margin:0 auto;padding:20px 18px 28px;overflow:hidden}\n.sky{position:absolute;left:0;right:0;top:0;height:96px;pointer-events:none}\n.sky i{position:absolute;width:3px;height:3px;border-radius:50%;background:#fff;opacity:.8;animation:tw 3s ease-in-out infinite}\n@keyframes tw{50%{opacity:.25}}\n.moon{position:absolute;top:14px;right:18px;width:56px;height:56px}\n.top{position:relative;padding-right:70px;margin-bottom:18px}\n.eyebrow{font-size:13px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#ffd23f;margin:0 0 4px}\nh1{font-size:25px;line-height:1.2;margin:0;letter-spacing:-.01em}\nh2{position:relative;font-size:17px;margin:0 0 10px;display:flex;align-items:center;gap:8px}\nh2 span{font-size:13px;font-weight:600;color:#d9ccf5}\n.grid{position:relative;display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:20px}\n.tile{font:inherit;color:#fff;border:2px solid rgba(255,255,255,.14);background:rgba(255,255,255,.09);border-radius:20px;min-height:96px;padding:10px 4px 8px;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;transition:transform .18s,box-shadow .18s,background .18s,border-color .18s}\n.tile b{font-size:38px;line-height:1;font-weight:400}\n.tile small{font-size:12.5px;font-weight:600;line-height:1.15;text-align:center;color:#ece4ff}\n.tile:active{transform:scale(.96)}\n.tile.on{background:rgba(255,210,63,.18);border-color:#ffd23f;transform:scale(1.06);box-shadow:0 0 0 4px rgba(255,210,63,.25),0 0 24px rgba(255,210,63,.55)}\n.len{position:relative;display:grid;grid-template-columns:repeat(3,1fr);gap:4px;background:rgba(0,0,0,.22);border-radius:16px;padding:4px;margin-bottom:20px}\n.len button{font:inherit;font-size:15px;font-weight:700;border:0;border-radius:12px;min-height:52px;background:transparent;color:#ece4ff;cursor:pointer;transition:background .18s,color .18s}\n.len button small{display:block;font-size:12px;font-weight:600;opacity:.85}\n.len button.on{background:#fff;color:#2b1b5a}\n.preview{position:relative;background:rgba(255,255,255,.1);border:1px dashed rgba(255,255,255,.35);border-radius:18px;padding:14px 16px;margin-bottom:18px;font-size:17px;line-height:1.45;min-height:76px}\n.preview em{font-style:normal;color:#ffd23f;font-weight:700}\n.preview .ph{color:#d9ccf5}\n.main{position:relative;width:100%;font:inherit;font-size:19px;font-weight:800;border:0;border-radius:18px;min-height:60px;background:#ffd23f;color:#2b1b5a;cursor:pointer;box-shadow:0 6px 0 #c99a00;transition:transform .1s,box-shadow .1s,background .18s}\n.main:active{transform:translateY(3px) scale(.98);box-shadow:0 3px 0 #c99a00}\n.main:disabled{background:#8c7bb8;color:#2b1b5a;box-shadow:none;cursor:default}\n.alt{position:relative;width:100%;font:inherit;font-weight:600;border:0;background:none;color:#d9ccf5;min-height:48px;margin-top:8px;cursor:pointer}\n.status{position:relative;text-align:center;color:#ece4ff;font-size:14px;min-height:20px;margin-top:6px}\n</style>\n<div class=\"wrap\">\n  <div class=\"sky\" id=\"sky\"></div>\n  <svg class=\"moon\" viewBox=\"0 0 56 56\" aria-hidden=\"true\"><path d=\"M38.3 9.8A22 22 0 1 0 47.9 37.9A20 20 0 0 1 38.3 9.8Z\" fill=\"#ffe89a\"/><circle cx=\"18\" cy=\"36\" r=\"2.5\" fill=\"#f2d36b\"/><circle cx=\"14\" cy=\"24\" r=\"1.8\" fill=\"#f2d36b\"/></svg>\n  <div class=\"top\"><p class=\"eyebrow\" id=\"eyebrow\"></p><h1 id=\"title\"></h1></div>\n  <div id=\"rows\"></div>\n  <h2 id=\"lenTitle\"></h2>\n  <div class=\"len\" id=\"len\"></div>\n  <div class=\"preview\" id=\"preview\"></div>\n  <button class=\"main\" id=\"go\" disabled></button>\n  <button class=\"alt\" id=\"surprise\"></button>\n  <div class=\"status\" id=\"status\"></div>\n</div>\n<script>\n// answer.data: {hero:\"fox\", place:\"volcano\", twist:\"key\", minutes:6, surprise:false}\n// The card's content. Change only this object.\nvar CARD = {\n  eyebrow: 'Bedtime',\n  title: 'Zosia, build your story!',\n  rows: [\n    {key:'hero', title:\"Who's the hero?\", options:[\n      {id:'fox', emoji:'🦊', label:'Fox', phrase:'A brave fox'},\n      {id:'dragon', emoji:'🐉', label:'Dragon', phrase:'A little dragon'},\n      {id:'robot', emoji:'🤖', label:'Robot', phrase:'A clever robot'},\n      {id:'mermaid', emoji:'🧜', label:'Mermaid', phrase:'A kind mermaid'}]},\n    {key:'place', title:'Where?', options:[\n      {id:'castle', emoji:'🏰', label:'Castle', phrase:'in a sparkly castle'},\n      {id:'volcano', emoji:'🌋', label:'Volcano island', phrase:'on a volcano island'},\n      {id:'space', emoji:'🚀', label:'Space', phrase:'far out in space'},\n      {id:'forest', emoji:'🌲', label:'Forest', phrase:'in a whispering forest'}]},\n    {key:'twist', title:'What happens?', options:[\n      {id:'party', emoji:'🎂', label:'Surprise party', phrase:'plans a surprise party'},\n      {id:'key', emoji:'🗝️', label:'Lost key', phrase:'finds a lost key'},\n      {id:'rainbow', emoji:'🌈', label:'Rainbow bridge', phrase:'crosses a rainbow bridge'},\n      {id:'friend', emoji:'🐣', label:'New friend', phrase:'meets a new friend'}]}\n  ],\n  lenTitle: 'How long?',\n  lengths: [{label:'Short', minutes:3}, {label:'Medium', minutes:6}, {label:'Long', minutes:10}],\n  defaultMinutes: 6,\n  placeholder: 'Pick a hero, a place and what happens…',\n  go: 'Tell my story ✨',\n  goNotReady: 'Pick all three first',\n  surprise: 'Surprise me!',\n  sending: 'Sending…',\n  doneText: 'Story time! Get cosy 🌙',\n  preview: 'Preview only: open this card in Bridgit to answer.'\n};\nvar $ = function(id){return document.getElementById(id)};\nvar pick = {}, minutes = CARD.defaultMinutes, sent = false, surprised = false;\n$('eyebrow').textContent = CARD.eyebrow; $('title').textContent = CARD.title;\n$('lenTitle').textContent = CARD.lenTitle; $('surprise').textContent = CARD.surprise;\nfor (var s = 0; s < 16; s++) {\n  var st = document.createElement('i');\n  st.style.left = (Math.random()*100) + '%'; st.style.top = (Math.random()*100) + '%';\n  st.style.animationDelay = (Math.random()*3) + 's';\n  if (s % 5 === 0) { st.style.width = st.style.height = '4px'; }\n  $('sky').appendChild(st);\n}\nCARD.rows.forEach(function(row){\n  var h = document.createElement('h2'); h.textContent = row.title; $('rows').appendChild(h);\n  var g = document.createElement('div'); g.className = 'grid'; $('rows').appendChild(g);\n  row.options.forEach(function(o){\n    var b = document.createElement('button'); b.className = 'tile';\n    var e = document.createElement('b'); e.textContent = o.emoji;\n    var l = document.createElement('small'); l.textContent = o.label;\n    b.appendChild(e); b.appendChild(l); g.appendChild(b);\n    o.el = b;\n    b.onclick = function(){ if (sent) return; pick[row.key] = o; draw(); };\n  });\n});\nCARD.lengths.forEach(function(L){\n  var b = document.createElement('button'); b.textContent = L.label;\n  var m = document.createElement('small'); m.textContent = L.minutes + ' min'; b.appendChild(m);\n  L.el = b; b.onclick = function(){ if (sent) return; minutes = L.minutes; draw(); };\n  $('len').appendChild(b);\n});\nfunction draw(){\n  CARD.rows.forEach(function(row){ row.options.forEach(function(o){ o.el.classList.toggle('on', pick[row.key] === o) }) });\n  CARD.lengths.forEach(function(L){ L.el.classList.toggle('on', L.minutes === minutes) });\n  var p = $('preview'); p.textContent = '';\n  var ready = pick.hero && pick.place && pick.twist;\n  if (!pick.hero && !pick.place && !pick.twist) {\n    var ph = document.createElement('span'); ph.className = 'ph'; ph.textContent = CARD.placeholder; p.appendChild(ph);\n  } else {\n    var parts = [[pick.hero, 'Someone'], [pick.twist, 'does something'], [pick.place, 'somewhere']];\n    parts.forEach(function(pt, i){\n      if (i) p.appendChild(document.createTextNode(' '));\n      var em = document.createElement(pt[0] ? 'em' : 'span');\n      if (!pt[0]) em.className = 'ph';\n      em.textContent = pt[0] ? pt[0].phrase : pt[1];\n      p.appendChild(em);\n    });\n    p.appendChild(document.createTextNode('…'));\n  }\n  $('go').disabled = !ready;\n  $('go').textContent = ready ? CARD.go : CARD.goNotReady;\n}\ndraw();\nfunction answer(){\n  if (sent) return;\n  var b = window.Bridgit || window.Bridget;\n  if (!b) { $('status').textContent = CARD.preview; return; }\n  sent = true;\n  document.querySelectorAll('button').forEach(function(x){ x.disabled = true });\n  $('status').textContent = CARD.sending;\n  b.respond(JSON.stringify({hero:pick.hero.id, place:pick.place.id, twist:pick.twist.id, minutes:minutes, surprise:surprised}));\n}\n$('go').onclick = answer;\n$('surprise').onclick = function(){\n  if (sent) return;\n  CARD.rows.forEach(function(row){ pick[row.key] = row.options[Math.floor(Math.random()*row.options.length)] });\n  surprised = true; draw();\n};\ndocument.addEventListener('bridgit:answered', function(e){\n  e.preventDefault();\n  $('title').textContent = '✓ ' + CARD.doneText;\n  $('status').textContent = '';\n  setTimeout(function(){ $('title').scrollIntoView({block:'center'}) }, 400);\n});\n</script>\n",
  "expires_in_seconds": 7200
}
Raw JSONhtmlkidsemojichoice

Make your own

  • Questions take 1 to 6 controls: buttons, text fields, choices (2 to 8 options) and links (https, tel, geo, mailto or an app). Titles up to 80 characters, text up to 2,000, labels up to 40.
  • Notices need no answer and can carry links. Add "speak": true to read one aloud and "priority": "urgent" when it can't wait.
  • HTML cards are a page you write, up to 256 KB, shown on about 90% of the screen. They run offline: no network, no remote fonts or images, pictures only as data:image/png, jpeg, gif or webp (or inline <svg> elements). Call Bridgit.respond(JSON.stringify({...})) once (window.Bridget on Bridgit 1.6.269 and older), up to 16 KB. Use buttons rather than a <form> submit, and plain padding rather than env(safe-area-inset-bottom).
  • Text panels are a live view you stream into while you work.