← BGP Desk
API
Tokens

Run BGP Desk from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page parses the evidence - every summary table, neighbor detail block and the router bgp section, secrets masked first - and applies the skill's state table, transport and policy checks to every peer: the down states with their first checks, Established with zero prefixes, loopback eBGP with no multihop, references the pasted definitions do not hold, max-prefix percentages, flapping and unbounded AS-path regex, then hands it all to the run as facts. The run writes the diagnosis or the change plan; the page then holds it to its own facts and re-reads a plan's configuration with the same parser (bgpreply.js).

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"bgp-desk"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, router, evidence, facts) is missing. The page always sends all six of task, router, evidence, symptom, carry, facts; symptom and carry are plain sentences when there is nothing to send.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - read, then plan:

A missing or unknown task is answered as diagnose. Both lanes send the same six fields. The facts are the page's parse - the peers, the findings with their ids, the peers needing a cause, the definitions and the read-only commands per peer - and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the diagnosis's decisions into carry for the change lane. Every password, key and community is replaced by <removed> before anything is sent. Evidence longer than 24,000 characters is cut on whole lines: the summaries, the router bgp section, the definitions and the detail of every peer with a finding go whole, healthy peers' detail is trimmed, and every cut is marked; every finding is still in the facts.

The input

FieldRequiredWhat
taskyesdiagnose or change. If it is missing or neither, do diagnose and set "task": "diagnose".
routeryesthe router's name, its platform (Cisco IOS / IOS-XE, Cisco NX-OS, Cisco IOS-XR or Arista EOS), its local AS and router-id.
evidenceyesthe pasted show output and configuration, every secret replaced by <removed>. A line starting "[... N lines" and ending "cut by the page" marks evidence the page parsed but did not send.
symptomsend it (a sentence when none)what the operator sees, in their words, or a sentence saying none was described.
carrysend it (a sentence when none)change task: the diagnosis's decisions, one per line as id | confirmed or cleared | peer (vrf) | cause or reason, and one line per peer as cause | peer (vrf) | state | confidence | cause; otherwise a sentence saying nothing was carried.
factsyesthe page's parse: the peers (peer | vrf | remote AS | type | state | prefixes per address family | up/down | last reset [class] | update source | policy), the findings (id | rule | severity | peer | evidence | first check | the page's value), the peers needing a cause, the definitions in the paste, and the read-only commands per peer in this platform's syntax. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @affaan-m/network-bgp-diagnostics - the only standard the run applies:

House rules

The replies

diagnose

Read-only. Confirm or clear each of the page's findings, name the most likely cause for every peer that is down or empty, say what the paste cannot show, and list the read-only commands to run next. Never propose a reset, a configuration line or any change in this task.

  1. summary - one sentence, at most 40 words: what is broken for the network today and its most likely cause.
  2. findings - one object per page finding you confirm, in id order: id (the page's id), peer (the address as the facts write it; "" for a router-wide finding), vrf ("default" when none; "" for a router-wide finding), cause (one sentence: why it happens, tied to the evidence), evidence (the line or counter that shows it, quoted briefly), check (one sentence: the first read-only check, from the skill's state table, transport or policy checks).
  3. cleared - one object per page finding that is not a problem in this evidence, with id and why (one sentence tied to the evidence). Every page id appears exactly once, in findings or in cleared.
  4. causes - one object for each peer the facts list under "Peers needing a cause", matched by peer and vrf together: peer, vrf, state (as the facts give it), cause (one sentence: the most likely cause), confidence (likely - the evidence shows it; possible - the evidence fits it without proving it; unknown - the paste cannot tell). [] when the facts list none.
  5. unseen - 1 to 5 short strings: what this paste cannot show that matters here - for example the peer's side of the session, an ACL or firewall on the path, received routes when received-route storage is off, other address families or VRFs, logs from before the paste.
  6. next - 3 to 12 objects, the read-only commands to run next, in order (for a down peer, transport before policy; for an Established peer with zero prefixes, policy before transport): command (exactly as typed on this platform - take it from the facts' read-only commands or write one in the same form), why (one sentence), ids (the finding ids it serves, e.g. "F1, F2"; "" when none). Only show, ping and traceroute commands - never clear, configure, debug or anything that changes state. An AS-path regex in a command carries token boundaries (_64499_, ^64499$).
  7. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "diagnose",
  "summary": "one sentence",
  "findings": [ { "id": "F1", "peer": "198.51.100.2", "vrf": "default", "cause": "one sentence", "evidence": "BGP state = Active; No active TCP connection", "check": "one sentence" } ],
  "cleared": [ { "id": "F3", "why": "one sentence" } ],
  "causes": [ { "peer": "198.51.100.2", "vrf": "default", "state": "Active", "cause": "one sentence", "confidence": "likely" } ],
  "unseen": [ "one short string" ],
  "next": [ { "command": "show ip route 198.51.100.2", "why": "one sentence", "ids": "F1" } ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in bgpreply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has a cause and a first check and sits on the page's peer and VRF; every peer the facts list as down or empty has exactly one cause, matched by peer and VRF together, and no cause names a peer the page never parsed; at least one thing the paste cannot show; 3 to 12 next commands, every one read-only (show, ping, traceroute - never clear, configure or debug) and any AS-path regex in them token-bounded; no reset proposed in the prose; a summary; no web link and no talk of the request's field names.

change

A change-window plan for the findings a configuration change fixes, written to be reviewed before anyone applies it.

  1. summary - one sentence, at most 40 words: what the plan changes, whether any session is reset, and what it leaves alone.
  2. steps - one object per change, in the order to apply them: ids (the finding ids this step fixes, e.g. "F1, F2"; an id appears in one step only), peer (the address as the facts write it; "" for a router-wide change), vrf ("default" when none), config (the exact configuration lines for this platform, starting from the mode line - for example "router bgp 64496\n neighbor 198.51.100.2 ebgp-multihop 2" - each line separated by \n inside the JSON string; where the facts give the page's value, use it verbatim), why (one sentence). Never put a clear in a step. eBGP sourced from a loopback with no multihop: ebgp-multihop 2 when the loopbacks are one link apart (or ttl-security hops, or disable-connected-check, when the facts show the platform uses it), and say the peer needs the matching change and a route back to this router's loopback. A reference to a route-map, prefix-list or AS-path list the paste does not define: point the reference at the closest defined name the facts give (on IOS a route-map match takes "no match ..." for the old name first, inside the same route-map sequence), or define the missing one only when the evidence shows what it must contain. A neighbor activated in no address family: activate it in the family the facts name. A max-prefix limit nearly reached: raise it to the page's value only when the evidence shows legitimate growth; otherwise keep the finding and say what to check first. IOS-XR eBGP with no route-policy: attach the intended policy in the missing direction. A state finding (Active, Idle, OpenSent, zero prefixes, a reset reason, flapping) is fixed by the step that removes its cause - list its id in that step. When no configuration change on this router fixes it (the cause is on the peer or the path), put it in kept.
  3. reset - [] unless a step changes the inbound or outbound policy of an Established session; then one object per such peer: peer, vrf, command (the soft or route-refresh form for that one peer on this platform, e.g. clear ip bgp 203.0.113.1 soft in, or clear bgp ipv4 unicast 203.0.113.1 soft in - never a hard clear and never *), why (one sentence: why it is safe - it re-applies policy without dropping the TCP session, and the evidence shows the peer supports route refresh when it does). A session that is not Established needs no reset; adding the missing setting lets it form.
  4. rollback - one string: the configuration lines that undo every step, in reverse order, separated by \n.
  5. verify - 2 to 8 read-only commands that prove the change worked, in this platform's syntax (the summary, the neighbor's state and last reset, its received routes, the max-prefix line).
  6. kept - one object per page finding you do not change: id, why (one sentence - the diagnosis cleared it, the fix is on the peer or the path, or it needs evidence first). Every page id appears exactly once, in steps or in kept; an id the carried diagnosis cleared belongs in kept.
  7. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "change",
  "summary": "one sentence",
  "steps": [ { "ids": "F1, F2", "peer": "198.51.100.2", "vrf": "default", "config": "router bgp 64496\n neighbor 198.51.100.2 ebgp-multihop 2", "why": "one sentence" } ],
  "reset": [ { "peer": "203.0.113.1", "vrf": "default", "command": "clear ip bgp 203.0.113.1 soft in", "why": "one sentence" } ],
  "rollback": "router bgp 64496\n no neighbor 198.51.100.2 ebgp-multihop 2",
  "verify": [ "show bgp ipv4 unicast summary" ],
  "kept": [ { "id": "F3", "why": "one sentence" } ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; every page finding id appears exactly once across the steps' ids and kept; every step has configuration and a reason and is for a peer the page parsed; no step holds a clear, relaxes an ACL or control-plane policy, writes a secret (use <removed>) or opens router bgp under another AS; every reset is a BGP clear in its soft or route-refresh form for one peer of this router (never *, never all) with a reason; a rollback and 2 to 8 read-only verification commands when there are steps. Then the steps' configuration is appended to the paste and parsed again: a configuration finding a step claims to fix that still fires, or one the plan introduces (a reference to a list that does not exist), is flagged. Enabling soft-reconfiguration inbound is noted as a change-window action.

Worked examples

Three runs from the page's free samples (invented routers, documentation addresses, private ASNs): the diagnosis of an edge router whose eBGP peer to a transit loopback sits in Active, the change plan for the same router carrying that diagnosis, and the diagnosis of a router whose ISP session is Established with zero prefixes behind a missing prefix-list. Each input is exactly what the page sends; each reply is the saved reply the sample replays, generated with every figure taken from the engine - but the evidence, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

diagnose - An eBGP peer stuck in Active

The page read it as: Peers down - 2 critical, 0 major, 0 minor; findings F1 peer-active, F2 loopback-no-multihop.

Input (evidence is 3156 characters; the first 1398 are shown; facts is 3256 characters; the first 1395 are shown):

{
 "task": "diagnose",
 "router": "Router: edge-r1\nPlatform: Cisco IOS / IOS-XE (detected from the paste)\nLocal AS: 64496; router-id 192.0.2.1",
 "evidence": "edge-r1#show ip bgp summary\nBGP router identifier 192.0.2.1, local AS number 64496\nBGP table version is 412, main routing table version 412\n140 network entries using 34720 bytes of memory\n140 path entries using 18760 bytes of memory\n3/2 BGP path/bestpath attribute entries using 864 bytes of memory\nBGP using 54344 total bytes of memory\nBGP activity 152/12 prefixes, 160/20 paths, scan interval 60 secs\n\nNeighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd\n192.0.2.11      4        64496   31244   31260      412    0    0 3w2d          140\n198.51.100.2    4        64499       0       0        1    0    0 never    Active\nedge-r1#show bgp ipv4 unicast neighbors 198.51.100.2\nBGP neighbor is 198.51.100.2,  remote AS 64499, external link\n Description: transit-a loopback\n  BGP version 4, remote router ID 0.0.0.0\n  BGP state = Active\n  Neighbor sessions:\n    0 active, is not multisession capable (disabled)\n  Message statistics:\n    InQ depth is 0\n    OutQ depth is 0\n\n For address family: IPv4 Unicast\n  Session: 198.51.100.2\n  BGP table version 1, neighbor version 1/0\n  Output queue size : 0\n  Index 0, Advertise bit 0\n  Route map for incoming advertisements is RM-TRANSIT-IN\n  Route map for outgoing advertisements is RM-TRANSIT-OUT\n  Maximum prefixes allowed 2000\n  Threshold for warning message 80%\n\n  Address tracking is enabled, the RIB does have a route\n\n[... 1758 more characters cut from this display ...]",
 "symptom": "The new transit session to transit-a never came up after the turn-up window.",
 "carry": "Nothing is carried into a diagnosis - it works from the evidence and the facts.",
 "facts": "The page's parse of 80 lines (Cisco IOS / IOS-XE, detected): local AS 64496, router-id 192.0.2.1; 2 peers - 1 Established with prefixes, 0 Established with zero, 1 Active, 0 Connect, 0 OpenSent/OpenConfirm, 0 Idle, 0 Idle (Admin); 140 prefixes received in total; 1 secret masked as <removed>; verdict: Peers down - 2 critical, 0 major, 0 minor.\nPeers (peer | vrf | remote AS | type | state | prefixes per address family | up/down | last reset [class] | update source | policy):\n- 192.0.2.11 | default | 64496 | iBGP | Established | IPv4 Unicast 140 | 3w2d (1987200 s) | - | Loopback0 | -\n- 198.51.100.2 | default | 64499 | eBGP | Active | IPv4 Unicast - | never | never | Loopback0, password set (masked) | rm RM-TRANSIT-IN in, rm RM-TRANSIT-OUT out, max 2000 80%\nFindings (id | rule | severity | peer | evidence | first check | the page's value):\n- F1 | peer-active | critical | 198.51.100.2 | 198.51.100.2, AS 64499, eBGP is Active, never up; it has never reset; no active TCP connection; update source Loopback0. | first check: TCP session is not completing; check routing, source, ACLs, and peer reachability. Active does not always mean the remote side is down.\n- F2 | loopback-no-multihop | critical | 198.51.100.2 | 198.51.100.2, AS 64499, eBGP is sourced from Loopback0 (192.0.2.1) but has no ebgp-multihop, ttl-security or disable-connected-check, so the eBGP session expects a directly\n\n[... 1861 more characters cut from this display ...]"
}

Reply:

{
 "task": "diagnose",
 "summary": "The transit-a session to 198.51.100.2 never forms: it is sourced from Loopback0 towards the peer's loopback with no ebgp-multihop, so the router keeps the eBGP connection to one hop and TCP never completes.",
 "findings": [
  {
   "id": "F1",
   "peer": "198.51.100.2",
   "vrf": "default",
   "cause": "The TCP connection to the peer's loopback never completes, so BGP has sat in Active since it was configured and has never been up.",
   "evidence": "BGP state = Active; Connections established 0; No active TCP connection",
   "check": "Read the route to 198.51.100.2 and ping it sourced from Loopback0 - Active does not by itself mean transit-a is down."
  },
  {
   "id": "F2",
   "peer": "198.51.100.2",
   "vrf": "default",
   "cause": "The neighbor is a loopback that is not on a shared link, but without ebgp-multihop the router still applies the single-hop connected check, so the session can never start.",
   "evidence": "External BGP neighbor configured for connected checks (single-hop no-disable-connected-check); peering address NOT in same link",
   "check": "Trace to 198.51.100.2 from Loopback0 to count how many hops apart the two loopbacks are."
  }
 ],
 "cleared": [],
 "causes": [
  {
   "peer": "198.51.100.2",
   "vrf": "default",
   "state": "Active",
   "cause": "The loopback-sourced eBGP session has no ebgp-multihop, so the connection to the peer's loopback one link away is never attempted beyond a single hop.",
   "confidence": "likely"
  }
 ],
 "unseen": [
  "Whether transit-a peers from its own loopback and has a route back to 192.0.2.1.",
  "Whether an ACL or firewall on the link towards 203.0.113.2 drops TCP port 179.",
  "Transit-a's configuration, including its own multihop setting and the password it expects."
 ],
 "next": [
  {
   "command": "show ip route 198.51.100.2",
   "why": "Confirms the static route to the peer's loopback and the link it uses.",
   "ids": "F1"
  },
  {
   "command": "ping 198.51.100.2 source Loopback0",
   "why": "Proves the peer's loopback answers from this router's loopback, the address the session uses.",
   "ids": "F1, F2"
  },
  {
   "command": "traceroute 198.51.100.2 source Loopback0",
   "why": "Counts the hops between the loopbacks, which sets the multihop value the change needs.",
   "ids": "F2"
  },
  {
   "command": "show tcp brief | include 198.51.100.2|:179",
   "why": "Shows whether any TCP connection to port 179 is attempted at all.",
   "ids": "F1"
  },
  {
   "command": "show logging | include BGP|198.51.100.2",
   "why": "Reads any BGP or authentication messages for this peer before anything is changed.",
   "ids": "F1"
  },
  {
   "command": "show bgp ipv4 unicast neighbors 198.51.100.2 | include BGP state|Last reset|Local host|Foreign host",
   "why": "Re-reads the state, last reset and TCP hosts after the checks.",
   "ids": "F1"
  }
 ],
 "caveats": [
  "The route to 198.51.100.2 is a static via 203.0.113.2; the session follows that link."
 ]
}

What step 5 prints for it: F1: The TCP connection to the peer's loopback never completes, so BGP has sat in Active since it was configured and has never been up. | F2: The neighbor is a loopback that is not on a shared link, but without ebgp-multihop the router still applies the single-hop connected check, so the session can never start.

The page's check: kept - nothing to flag

change - An eBGP peer stuck in Active

The input carries the saved diagnosis (R.laneRouter fills carry from the diagnose reply), as the page's handoff does. The page appended the plan's configuration to the paste and parsed it again: 1 configuration findings before, 0 after.

Input (evidence is 3156 characters; the first 1398 are shown; facts is 3439 characters; the first 1395 are shown):

{
 "task": "change",
 "router": "Router: edge-r1\nPlatform: Cisco IOS / IOS-XE (detected from the paste)\nLocal AS: 64496; router-id 192.0.2.1",
 "evidence": "edge-r1#show ip bgp summary\nBGP router identifier 192.0.2.1, local AS number 64496\nBGP table version is 412, main routing table version 412\n140 network entries using 34720 bytes of memory\n140 path entries using 18760 bytes of memory\n3/2 BGP path/bestpath attribute entries using 864 bytes of memory\nBGP using 54344 total bytes of memory\nBGP activity 152/12 prefixes, 160/20 paths, scan interval 60 secs\n\nNeighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd\n192.0.2.11      4        64496   31244   31260      412    0    0 3w2d          140\n198.51.100.2    4        64499       0       0        1    0    0 never    Active\nedge-r1#show bgp ipv4 unicast neighbors 198.51.100.2\nBGP neighbor is 198.51.100.2,  remote AS 64499, external link\n Description: transit-a loopback\n  BGP version 4, remote router ID 0.0.0.0\n  BGP state = Active\n  Neighbor sessions:\n    0 active, is not multisession capable (disabled)\n  Message statistics:\n    InQ depth is 0\n    OutQ depth is 0\n\n For address family: IPv4 Unicast\n  Session: 198.51.100.2\n  BGP table version 1, neighbor version 1/0\n  Output queue size : 0\n  Index 0, Advertise bit 0\n  Route map for incoming advertisements is RM-TRANSIT-IN\n  Route map for outgoing advertisements is RM-TRANSIT-OUT\n  Maximum prefixes allowed 2000\n  Threshold for warning message 80%\n\n  Address tracking is enabled, the RIB does have a route\n\n[... 1758 more characters cut from this display ...]",
 "symptom": "The new transit session to transit-a never came up after the turn-up window.",
 "carry": "F1 | confirmed | 198.51.100.2 (default) | The TCP connection to the peer's loopback never completes, so BGP has sat in Active since it was configured and has never been up.\nF2 | confirmed | 198.51.100.2 (default) | The neighbor is a loopback that is not on a shared link, but without ebgp-multihop the router still applies the single-hop connected check, so the session can never start.\ncause | 198.51.100.2 (default) | Active | likely | The loopback-sourced eBGP session has no ebgp-multihop, so the connection to the peer's loopback one link away is never attempted beyond a single hop.",
 "facts": "The page's parse of 80 lines (Cisco IOS / IOS-XE, detected): local AS 64496, router-id 192.0.2.1; 2 peers - 1 Established with prefixes, 0 Established with zero, 1 Active, 0 Connect, 0 OpenSent/OpenConfirm, 0 Idle, 0 Idle (Admin); 140 prefixes received in total; 1 secret masked as <removed>; verdict: Peers down - 2 critical, 0 major, 0 minor.\nPeers (peer | vrf | remote AS | type | state | prefixes per address family | up/down | last reset [class] | update source | policy):\n- 192.0.2.11 | default | 64496 | iBGP | Established | IPv4 Unicast 140 | 3w2d (1987200 s) | - | Loopback0 | -\n- 198.51.100.2 | default | 64499 | eBGP | Active | IPv4 Unicast - | never | never | Loopback0, password set (masked) | rm RM-TRANSIT-IN in, rm RM-TRANSIT-OUT out, max 2000 80%\nFindings (id | rule | severity | peer | evidence | first check | the page's value):\n- F1 | peer-active | critical | 198.51.100.2 | 198.51.100.2, AS 64499, eBGP is Active, never up; it has never reset; no active TCP connection; update source Loopback0. | first check: TCP session is not completing; check routing, source, ACLs, and peer reachability. Active does not always mean the remote side is down.\n- F2 | loopback-no-multihop | critical | 198.51.100.2 | 198.51.100.2, AS 64499, eBGP is sourced from Loopback0 (192.0.2.1) but has no ebgp-multihop, ttl-security or disable-connected-check, so the eBGP session expects a directly\n\n[... 2044 more characters cut from this display ...]"
}

Reply:

{
 "task": "change",
 "summary": "One line lets the loopback-sourced eBGP session to 198.51.100.2 form across one link; nothing is reset, because the session is not up and will connect on its next retry.",
 "steps": [
  {
   "ids": "F1, F2",
   "peer": "198.51.100.2",
   "vrf": "default",
   "config": "router bgp 64496\n neighbor 198.51.100.2 ebgp-multihop 2",
   "why": "Allows the session between loopbacks one link apart (the page's value, 2); transit-a needs the matching multihop and a route back to 192.0.2.1."
  }
 ],
 "reset": [],
 "rollback": "router bgp 64496\n no neighbor 198.51.100.2 ebgp-multihop 2",
 "verify": [
  "show bgp ipv4 unicast summary",
  "show bgp ipv4 unicast neighbors 198.51.100.2 | include BGP state|Last reset|Local host|Foreign host",
  "show tcp brief | include 198.51.100.2|:179",
  "show bgp ipv4 unicast neighbors 198.51.100.2 routes"
 ],
 "kept": [],
 "caveats": [
  "Agree the change with transit-a first: their side needs ebgp-multihop too, or the session stays in Active."
 ]
}

What step 5 prints for it: F1, F2: Allows the session between loopbacks one link apart (the page's value, 2); transit-a needs the matching multihop and a route back to 192.0.2.1.

The page's check: kept - nothing to flag

diagnose - Established, but zero prefixes

The page read it as: Routes missing - 2 critical, 0 major, 1 minor; findings F1 zero-prefixes, F2 ref-missing, F3 regex-boundary.

Input (evidence is 3572 characters; the first 1394 are shown; facts is 3596 characters; the first 1389 are shown):

{
 "task": "diagnose",
 "router": "Router: branch-r7\nPlatform: Cisco IOS / IOS-XE (detected from the paste)\nLocal AS: 64497; router-id 192.0.2.21",
 "evidence": "branch-r7#show bgp ipv4 unicast summary\nBGP router identifier 192.0.2.21, local AS number 64497\nBGP table version is 2210, main routing table version 2210\n812 network entries using 201376 bytes of memory\n812 path entries using 108808 bytes of memory\n\nNeighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd\n192.0.2.22      4        64497   88210   88195     2210    0    0 5w1d          812\n203.0.113.1     4        64500    3121    3305     2210    0    0 2d04h           0\nbranch-r7#show bgp ipv4 unicast neighbors 203.0.113.1\nBGP neighbor is 203.0.113.1,  remote AS 64500, external link\n Description: isp-b\n  BGP version 4, remote router ID 203.0.113.1\n  BGP state = Established, up for 2d04h\n  Last read 00:00:21, last write 00:00:17, hold time is 180, keepalive interval is 60 seconds\n  Neighbor sessions:\n    1 active, is not multisession capable (disabled)\n  Neighbor capabilities:\n    Route refresh: advertised and received(new)\n    Four-octets ASN Capability: advertised and received\n    Address family IPv4 Unicast: advertised and received\n\n For address family: IPv4 Unicast\n  Session: 203.0.113.1\n  BGP table version 2210, neighbor version 2210/0\n  Route map for incoming advertisements is RM-ISP-IN\n  Route map for outgoing advertisements is RM-ISP-OUT\n                                 Sent       Rcvd\n  Prefix activity:               ----       ----\n\n[... 2178 more characters cut from this display ...]",
 "symptom": "Since the ISP migration on Saturday the branch has no routes from isp-b, but the session is up.",
 "carry": "Nothing is carried into a diagnosis - it works from the evidence and the facts.",
 "facts": "The page's parse of 92 lines (Cisco IOS / IOS-XE, detected): local AS 64497, router-id 192.0.2.21; 2 peers - 1 Established with prefixes, 1 Established with zero, 0 Active, 0 Connect, 0 OpenSent/OpenConfirm, 0 Idle, 0 Idle (Admin); 812 prefixes received in total; verdict: Routes missing - 2 critical, 0 major, 1 minor.\nPeers (peer | vrf | remote AS | type | state | prefixes per address family | up/down | last reset [class] | update source | policy):\n- 192.0.2.22 | default | 64497 | iBGP | Established | IPv4 Unicast 812 | 5w1d (3110400 s) | - | Loopback0 | -\n- 203.0.113.1 | default | 64500 | eBGP | Established | IPv4 Unicast 0 | 2d04h (187200 s) | Peer closed the session of session 1 [peer] | - | rm RM-ISP-IN in, rm RM-ISP-OUT out, max 1000 75%\nFindings (id | rule | severity | peer | evidence | first check | the page's value):\n- F1 | zero-prefixes | critical | 203.0.113.1 | 203.0.113.1, AS 64500, eBGP is Established in IPv4 Unicast with 0 prefixes received (up 2d04h); the neighbor detail shows 25 denied inbound by route-map; inbound policy RM-ISP-IN. | first check: Check inbound policy, max-prefix, advertised routes, and AFI/SAFI.\n- F2 | ref-missing | critical | 203.0.113.1 | route-map RM-ISP-IN seq 10 (applied inbound on 203.0.113.1) matches prefix-list PL-ISP-IN, and no prefix-list PL-ISP-IN is defined in the paste - defined: PL-ISP-V4-IN, PL-LOCAL-OUT. How a missing\n\n[... 2207 more characters cut from this display ...]"
}

Reply:

{
 "task": "diagnose",
 "summary": "isp-b at 203.0.113.1 is up but every route it sends is denied: the inbound route-map RM-ISP-IN matches PL-ISP-IN, a prefix-list this router does not define, so all 25 prefixes are dropped.",
 "findings": [
  {
   "id": "F1",
   "peer": "203.0.113.1",
   "vrf": "default",
   "cause": "All 25 prefixes the ISP sends are denied by the inbound route-map, so none are accepted although the session is healthy.",
   "evidence": "Prefixes Current: 1 sent, 0 received; route-map: 25 denied inbound",
   "check": "Read the route-map and the prefix-list by name before looking at transport - the session itself has been up for 2d04h."
  },
  {
   "id": "F2",
   "peer": "203.0.113.1",
   "vrf": "default",
   "cause": "Sequence 10 of RM-ISP-IN matches PL-ISP-IN, which is not defined, while PL-ISP-V4-IN is - the denied count fits a permit sequence that never matches, leaving only the implicit deny.",
   "evidence": "ip address prefix-lists: PL-ISP-IN; ip prefix-list PL-ISP-V4-IN: 3 entries",
   "check": "Show the prefix-list PL-ISP-IN by name to confirm it is absent, then read RM-ISP-IN sequence by sequence."
  },
  {
   "id": "F3",
   "peer": "",
   "vrf": "",
   "cause": "AS-path list 20 matches 64500 without token boundaries, so the outbound deny in RM-ISP-OUT would also catch paths that merely contain those digits inside a longer AS number.",
   "evidence": "AS path access list 20: permit 64500",
   "check": "Compare the routes matched by the token-bounded regex _64500_ with what the list is meant to stop."
  }
 ],
 "cleared": [],
 "causes": [
  {
   "peer": "203.0.113.1",
   "vrf": "default",
   "state": "Established with zero prefixes",
   "cause": "The inbound route-map RM-ISP-IN matches the undefined PL-ISP-IN and denies all 25 received prefixes.",
   "confidence": "likely"
  }
 ],
 "unseen": [
  "The change record of Saturday's migration - PL-ISP-V4-IN looks like the intended list, but the paste cannot prove it.",
  "The routes before policy: received-route storage is off, so the 25 denied prefixes cannot be listed here.",
  "Whether isp-b also changed what it advertises during the migration."
 ],
 "next": [
  {
   "command": "show ip prefix-list PL-ISP-IN",
   "why": "Confirms the list the route-map matches does not exist.",
   "ids": "F2"
  },
  {
   "command": "show route-map RM-ISP-IN",
   "why": "Reads each sequence of the inbound route-map and its match and set clauses.",
   "ids": "F1, F2"
  },
  {
   "command": "show ip prefix-list PL-ISP-V4-IN",
   "why": "Checks that the closest defined list really holds what isp-b should send.",
   "ids": "F2"
  },
  {
   "command": "show bgp ipv4 unicast neighbors 203.0.113.1 routes",
   "why": "Shows that no isp-b route is accepted today, the baseline for after the fix.",
   "ids": "F1"
  },
  {
   "command": "show bgp ipv4 unicast neighbors 203.0.113.1 advertised-routes",
   "why": "Confirms this router still advertises its own prefix to the ISP.",
   "ids": "F1"
  },
  {
   "command": "show ip as-path-access-list 20",
   "why": "Reads the AS-path list behind the outbound deny.",
   "ids": "F3"
  },
  {
   "command": "show bgp ipv4 unicast regexp _64500_",
   "why": "Lists the paths the token-bounded regex matches, to compare with the list's intent.",
   "ids": "F3"
  }
 ],
 "caveats": []
}

What step 5 prints for it: F1: All 25 prefixes the ISP sends are denied by the inbound route-map, so none are accepted although the session is healthy. | F2: Sequence 10 of RM-ISP-IN matches PL-ISP-IN, which is not defined, while PL-ISP-V4-IN is - the denied count fits a permit sequence that never matches, leaving only the implicit deny. | F3: AS-path list 20 matches 64500 without token boundaries, so the outbound deny in RM-ISP-OUT would also catch paths that merely contain those digits inside a longer AS number.

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./bgpkit.js"); require("./bgpreply.js")), then:

const K = BgpKit, R = BgpReply;
const f = K.router({ name: "lab-r1", platform: "auto", symptom: "", carry: "",
  evidence: "lab-r1#show ip bgp summary\nBGP router identifier 192.0.2.9, local AS number 64505\n\nNeighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd\n198.51.100.21   4        64506       0       0        1    0    0 never    Connect\n198.51.100.25   4        64507     511     498       30    0    0 1d02h           0" });
// f.peers (state, prefixes, up/down in seconds, last reset), f.findings (id, rule, sev, peer, vrf, evidence, first, next, fix), f.checklist
// K.peersCsv(f), K.findingsCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "diagnose");                       // or "change" (R.laneRouter(raw, "change", diagnosis) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "diagnose");
// R.check(reply, b)                                         // {status: kept or flags, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://bgp-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"bgp-desk"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "diagnose", "router": "Router: edge-r1
Platform: Cisco IOS / IOS-XE (detected from the paste)
Local AS: 64496; router-id 192.0.2.1", "evidence": "edge-r1#show ip bgp summary
...", "symptom": "The new transit session to transit-a never came up after the turn-up window.", "carry": "Nothing is carried into a diagnosis - it works from the evidence and the facts.", "facts": "The page's parse of 80 lines ...
Findings (id | rule | severity | peer | evidence | first check | the page's value):
- F1 | peer-active | critical | 198.51.100.2 | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses bgp-desk:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A diagnose reply carries summary, findings, cleared, causes, unseen, next and caveats; a change reply carries summary, steps, reset, rollback, verify, kept and caveats. The code below prints the gist of either: each confirmed finding and its cause, or each step.

KEY="bgp-desk:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "change" then (.steps[] | "\(.ids): \(.why)") 
       else (.findings[] | "\(.id): \(.cause)") end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: bgp-desk:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json