← Launch Gate / API
Get a token

Driving Launch Gate over HTTP

Everything the page does, you can do from a script. One endpoint does the work; the rest is authentication and polling. The app takes one proposed launch — a brief in free prose and a facts_text block of key: value lines describing what it touches — plus a task field naming which of three lanes to run over that one work object. It returns a single JSON envelope.

Base URL

https://api.skillsafe.ai/v1/app-api

The only headers on any call are Authorization: Bearer <token> and Content-Type: application/json — plus Idempotency-Key on /run and /run-stream. There is no slug header. The app slug is named in exactly one place: the JSON body of POST /guest, as {"slug":"launch-gate"}. If you would rather not touch DevTools, the token page shows and copies your token.

The response envelope

Every endpoint returns the same wrapper. Success carries data; failure carries error. Nothing returns a bare value, so a client can branch on the presence of error alone.

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

Error codes

CodeHTTPWhat it meansWhat to do
unauthorized401Missing, malformed or expired token, or a token minted against another app.Mint a new one with POST /guest and {"slug":"launch-gate"}, or copy a personal token from the token page. Guest tokens expire; personal tokens outlive them.
payment_required402Balance below the run's minimum, so no hold could be placed.Call /estimate first and compare min_credits against the credits from /me.
rate_limited429Too many requests in too short a window.Back off with jitter and retry the same idempotency key; never tight-loop a poll.
validation_error400The body did not match the app's shape — a missing brief, a non-string facts_text, malformed JSON.Read error.details; it names the offending field. Check first that you did not wrap the input in an input key.
not_found404No such job id, or a job that belongs to another subject.Poll the job_id the /run response gave you, with the same token that created it.
server_error500Something failed upstream of the model.Retry once with the same Idempotency-Key, which cannot double-bill. If it repeats, the run did not start and nothing was charged.

A job that starts and then fails is not an HTTP error. It comes back as a normal {"ok": true} envelope from GET /jobs/{job_id} with status set to failed, so always branch on status as well as on error.

The task field comes first

Launch Gate is a three-lane app. Every run must name its lane in task, because all three lanes share one system prompt and one model and are routed by that field alone. The contract is to produce that lane only and never a blend of two. If task is missing or is not one of the three ids, the model picks the closest lane, produces that lane's contract in full, and says so by setting lane to what it chose plus lane_inferred to true — a fallback for a malformed request, not a feature to rely on. Send the lane explicitly.

taskThe question it answersartifact.kindnext_lane.lane
applicabilityGiven this launch, which regulations, frameworks and internal approvals actually apply — and which plausibly-relevant ones do not? The scoping lane. It runs first and the other two work inside the scope it draws.nonerisk
riskHow bad is it, how likely, and who has to decide? Classifies on a severity-by-likelihood matrix, then applies escalation criteria. It does not re-scope.memoreadiness
readinessWhat evidence must exist, who owns it, and what is missing today? The producing lane: an audit-readiness register, one row per requirement.register""

The natural order through the app is applicabilityriskreadiness, and every response names what it thinks comes next in next_lane. On applicability, next_lane.lane is risk whenever any framework applies or any finding is high or above, and "" otherwise. readiness is the last lane, so its next_lane.lane is always the empty string.

The run input object

This is the exact JSON the page submits. It is the whole request body for /estimate, /run and /run-stream alike — see the warning below about the input wrapper that does not exist.

{
  "task": "applicability",
  "brief": "Weekly churn-risk email. We score every workspace nightly ... (free prose)",
  "facts_text": "initiative: Weekly churn-risk email\nowner: Priya Raman, PM Growth\nlaunch: 2026-09-15\nregions: EU, UK, California\ndata: email, name, product usage events, IP address\npurpose: rank accounts by churn risk and alert the owner\nprocessors: Segment, SendGrid, Snowflake (US)\ntransfers: EU -> US\nautomated-decision: yes - accounts scoring below 0.3 are auto-downgraded\nretention: 24 months\nframeworks: GDPR, SOC 2, ISO 27001\nsecurity-review: not started\ndpa: SendGrid signed 2025-03; Segment pending",
  "context": "optional free text - constraints, what has already been reviewed",
  "prescan": {
    "signals": {
      "regions": [{"id": "eu", "label": "EU/EEA", "declared": true}],
      "categories": [{"id": "email", "label": "email address", "special": false, "declared": true}],
      "frameworks": [{"id": "GDPR", "name": "EU General Data Protection Regulation", "trigger": "regions: EU"}],
      "processors": [{"name": "SendGrid", "dpa_status": "signed"}],
      "launch_in_days": 27,
      "automated_decision": true,
      "cross_border": true
    },
    "flags": [
      {"id": "GS-AUTOMATED-DECISION", "severity": "critical", "label": "Automated decision with a significant effect", "detail": "accounts scoring below 0.3 are auto-downgraded"}
    ],
    "gates": [{"gate": "DPIA", "role": "dpo"}]
  },
  "clip_note": "present only when the paste was clipped"
}

Input fields

FieldTypeRequiredNotes
taskstringyesOne of applicability, risk or readiness. Send it. The lane is the single most consequential field in the body.
briefstringyesThe launch brief in free prose: a product feature, a marketing programme, a data-sharing arrangement, a new vendor, a market entry. What is being proposed and why.
facts_textstringin practice yesThe facts block, one key: value line each. The keys the app and the scanner both read are initiative, owner, launch, regions, data, purpose, processors, transfers, automated-decision, retention, frameworks, security-review and dpa. The facts block is authoritative where it conflicts with the brief, and the model says so in assumptions when the two disagree.
contextstringnoFree text: constraints, what has already been reviewed, internal risk appetite, who the reader is.
carriedstring or objectnoA digest of the previous lane's output, which the page sends when the user takes a handoff button. The risk lane works inside the scope carried establishes rather than re-scoping it.
prescanobjectno{signals, flags, gates} — deterministic facts read out of the same material by the in-browser scanner, which the model must reconcile. See the next section.
clip_notestringnoSend only when the paste was too long to transmit whole; it names what was cut. The model will not comment on material the clip note says it was not shown.
retry_notestringnoSent only on an automatic re-ask after a reply that failed to parse. It quotes the parse error and restates the contract. Reuse the idempotency key with the attempt number incremented so a retry cannot double-bill.

The output envelope

All three lanes return exactly the same outer object. Only body is per-lane. The reply is one JSON object and nothing else — no prose before it and no code fence around it — and it arrives as a string in data.output.output, so you parse it yourself.

{
  "lane": "applicability",
  "lane_inferred": false,
  "initiative": "short name for what is being launched, 2-8 words",
  "title": "one line naming the lane's output for this initiative",
  "posture": "clear-to-ship | conditions-first | blocked",
  "verdict": "one sentence a VP can read and act on",
  "summary": "3-6 sentences for the approver, ending with the not-legal-advice line",
  "jurisdictions": ["EU/EEA", "United Kingdom", "California"],
  "frameworks": [
    {"id": "GDPR", "name": "EU General Data Protection Regulation",
     "applies": "yes | likely | no", "basis": "Art. 3(2) - offering services to EU data subjects",
     "why": "one sentence tying it to a fact in the input"}
  ],
  "assumptions": ["..."],
  "open_questions": ["..."],
  "findings": [
    {"id": "LG-001", "title": "short imperative title", "severity": "critical|high|medium|low",
     "area": "privacy|security|consumer-protection|marketing|ai-governance|accessibility|records|contracts|sector-specific|cross-border|internal-approval",
     "requirement": "the obligation in one clause, with its provision",
     "evidence": "at most one sentence quoted or paraphrased from the input",
     "why": "why it bites here",
     "action": "the single next action, imperative",
     "owner_role": "privacy-counsel"}
  ],
  "coverage_check": [
    {"flag_id": "GS-CROSS-BORDER", "status": "confirmed|set-aside|superseded",
     "finding_id": "LG-003", "note": "one sentence"}
  ],
  "approvals": [
    {"role": "privacy-counsel", "decision_needed": "sign off the transfer basis",
     "blocking": true, "by_when": "before launch | before general availability | within 30 days of launch"}
  ],
  "artifact": {"kind": "none|markdown|register|memo", "filename": "", "content": ""},
  "next_lane": {"lane": "risk", "reason": "one sentence"},
  "body": { }
}

The prescan contract

The browser app runs a deterministic scan over the brief and the facts block before every run and passes what it read in prescan. It has three parts, and they are not interchangeable: signals is what the scan read, flags is what it objects to, and gates is the approvals it derived. The model must return exactly one coverage_check entry per flag id sent, and none for an id that was not sent. That is what makes a free scan able to hold a paid run accountable — anything unaccounted for is a defect you can detect programmatically:

sent    = {f["id"] for f in payload["prescan"]["flags"]}
covered = {c["flag_id"] for c in result["coverage_check"]}
assert sent == covered, f"unreconciled: {sent - covered}; invented: {covered - sent}"

status is one of three values. confirmed — the flag is a real issue, and finding_id names the LG-* finding that carries it. set-aside — the rule fires but does not matter on this material, and note says why in one sentence. superseded — a different, larger finding subsumes it, and finding_id names that one. A flag is never silently dropped.

prescan.signals

Signals are facts, not problems. They exist so the model does not have to re-derive from prose what a parser already read out of the facts block, and so that a disagreement about a countable thing — which regions were declared, how many days to launch, whether a DPA is signed — resolves in favour of the scan, which is deterministic where the model is not. Signals need no coverage_check entry.

KeyTypeWhat it holds
regionsarray{id, label, declared} per region read from the regions line. declared is true when the facts block named it outright rather than the brief implying it.
categoriesarray{id, label, special, declared} per data category. special marks a special category of personal data, which is the field that changes the lawful-basis analysis.
frameworksarray{id, name, trigger} per framework the scan believes is pulled in, with the input line that pulled it. The model may disagree, and says so by returning applies as likely or no with a reason.
processorsarray{name, dpa_status} per named processor. dpa_status is read straight from the dpa line: signed, pending or unknown.
launch_in_daysnumberDays from today to the launch date, or 0 when no date was given. This is the only date arithmetic in the system: the prompt is forbidden from inventing a deadline in days.
automated_decisionbooleanWhether the automated-decision line says yes. Drives the GDPR Art. 22 line of analysis.
cross_borderbooleanWhether the transfers line describes a transfer out of a declared region.

prescan.flags

Each flag is {id, severity, label, detail}. id is the stable rule id, a GS-* string, and it is the value that must come back as a coverage_check.flag_id. severity is critical, high, medium or low, and flags arrive sorted by severity in that order. detail carries the countable fact or the quoted fragment the rule fired on, and is an empty string when the rule has nothing to add.

"flags": [
  {"id": "GS-AUTOMATED-DECISION", "severity": "critical",
   "label": "Automated decision with a significant effect",
   "detail": "accounts scoring below 0.3 are auto-downgraded"},
  {"id": "GS-CROSS-BORDER", "severity": "high",
   "label": "Personal data leaves a declared region",
   "detail": "EU -> US"},
  {"id": "GS-SECURITY-REVIEW-MISSING", "severity": "high",
   "label": "No security review has started",
   "detail": "security-review: not started"},
  {"id": "GS-DPA-PENDING", "severity": "medium",
   "label": "A named processor has no signed agreement",
   "detail": "Segment pending"}
]

Treat the GS-* ids as an interface, and treat the set of them as data. Renaming an id breaks the reconciliation contract in both directions: the model has no entry to return and your assertion has nothing to match. The four above are what the launch bundled with the app produces; a different launch produces a different set, and the same launch run through two lanes can produce two different sets, because the scan sends only the rules that belong to the lane you are about to run. Compute prescan per lane rather than caching one lane's flags and resending them on another.

prescan.gates

gates is an array of {gate, role} — the approvals the scan derived from the signals, such as {"gate": "DPIA", "role": "dpo"} once automated_decision is true. They are a starting point, not a verdict: the applicability lane returns its own body.gates with a required field of yes, likely or no, and the envelope's approvals array lists only the gates the input actually raises. Gates need no coverage_check entry either.

You may send an empty prescan, or omit it entirely. The lane still runs; it simply has fewer deterministic facts to ground itself in, coverage_check comes back as an empty array, and nothing checks the model's reading of the facts block for you.

1. A tiny client

A few lines of setup that every later step reuses: the base URL, the bearer token, and a JSON post that raises on the error branch of the envelope. Two headers, no more — Authorization and Content-Type. The slug constant is here only because step 2 needs it in a request body; it never becomes a header. Replace the "YOUR_TOKEN" placeholder by reading the token out of the environment or out of wherever your program keeps secrets, rather than committing it. If you would rather not touch DevTools, the token page shows and copies your token.

2. Get a token

A guest token is minted on demand and is enough for /me and /estimate. Running a lane is metered, so it needs a personal token — sign in at the token page and copy it from there. This is the one and only call that names the app: {"slug": "launch-gate"} in the JSON body, and note that this call needs no Authorization header at all. Every POST /guest mints a new guest identity, so reuse one token across a session rather than minting per request.

3. Check who you are and what you can spend

GET /me is free and tells you the subject_typeguest or user — and the credit balance. Compare that balance against the estimate in the next step before you run anything: a 402 after a reviewer has already pasted a launch brief is a failure of the client, not of the user.

4. Price the run before you make it

POST /estimate is free, creates no job and charges nothing. It takes the same body the run will take and returns the model, the markup and the two numbers that matter. model_alias is gpt-terra and markup_bps is 1000, which is the app's ten-percent metered markup. hold_credits is the amount reserved against your balance, priced at the full output cap; the actual charge is usually far lower, so present it as reserved and never as the price. min_credits is the floor you must be able to cover for the run to be accepted at all. sponsor_enabled tells you whether the app owner is covering runs for this subject.

The body is the input object itself. Do not wrap it in an input key.

POST /estimate, POST /run and POST /run-stream all take {"task": ..., "brief": ..., "facts_text": ...} at the top level of the body. Sending {"input": {"task": ...}} instead is the one mistake worth calling out, because it does not error. The request returns 200 with a plausible-looking hold, the run is accepted, you are billed — and the model never sees task, brief or facts_text at all. What comes back is a lane the model guessed at over an empty record, usually with lane_inferred set to true and an initiative of unknown. Assert on the top-level shape of your payload before you post it, and check lane_inferred on every reply.

The hold differs per lane. The three lanes have different output caps — a readiness run that has to produce a register of a dozen rows plus the Markdown file reserves considerably more than an applicability read. Estimate the lane you are about to run, and re-estimate whenever task changes.

5. Run a lane and poll for the result

POST /run takes the input object as its body and returns a job_id immediately; poll GET /jobs/{job_id} until status is terminal (succeeded, failed or cancelled). Two seconds between polls is plenty. The model's reply is a string at data.output.output, so parse it yourself.

Always send an Idempotency-Key on /run and /run-stream, and derive it from three parts:

launch-gate:<lane>:<hash of input>:a<attempt>

launch-gate:applicability:9f2c1a7be40d5c31:a1
launch-gate:risk:9f2c1a7be40d5c31:a1          # same brief, different lane, different key
launch-gate:risk:9f2c1a7be40d5c31:a2          # deliberate re-ask after a parse failure

All three parts matter. Include the lane or the second lane over the same brief collides with the first and hands you back the first lane's cached result — you would ask for a risk matrix and get an applicability scope, with no error to tell you why. Include a hash of the input — the brief, the facts block and the context together — so editing one line of the facts block starts a new run rather than replaying the old one. Include an attempt counter so a deliberate re-ask is a new run while a transport-level retry of the same attempt reuses the key and cannot double-bill.

6. Stream it instead, for anything interactive

POST /run-stream is the same call with the same body over Server-Sent Events. Deltas arrive as they are generated, which is what the page uses to advance its progress stages while a register is being written. The same Idempotency-Key rule applies, and a done event closes the stream with the job id and what was actually charged. Accumulate the deltas and parse the JSON once the stream closes — a half-received envelope is not valid JSON, and half a risk matrix is worse than none.

7. One worked example per lane

All three requests below send the same brief and the same facts_text — the weekly churn-risk email that the app ships as its free example — and differ in task and in the lane-filtered prescan. The envelope is identical across all three; only body and artifact change. Arrays shown with a trailing ... follow the shapes documented above, abbreviated here to one or two representative entries.

task: "applicability" — Draw the scope

Which frameworks are in scope and on what trigger, which plausibly-relevant ones are out and why, what data is being processed for what purpose on what candidate lawful basis, which approval gates the launch raises, and what the privacy notice will have to say. Be decisive about what is out: a launch cleared of HIPAA in one sentence is worth more to the reader than a register that lists it as "consider". This is the only lane that emits no file.

Request

{
  "task": "applicability",
  "brief": "<the launch brief>",
  "facts_text": "<the key: value facts block>",
  "context": "Security review has not started. Legal has seen the model card but not the transfer basis.",
  "prescan": {
    "signals": {
      "regions": [{"id": "eu", "label": "EU/EEA", "declared": true}, {"id": "uk", "label": "United Kingdom", "declared": true}, {"id": "ca-us", "label": "California", "declared": true}],
      "categories": [{"id": "email", "label": "email address", "special": false, "declared": true}, {"id": "ip", "label": "IP address", "special": false, "declared": true}],
      "frameworks": [{"id": "GDPR", "name": "EU General Data Protection Regulation", "trigger": "regions: EU"}],
      "processors": [{"name": "SendGrid", "dpa_status": "signed"},
                     {"name": "Segment", "dpa_status": "pending"}],
      "launch_in_days": 27,
      "automated_decision": true,
      "cross_border": true
    },
    "flags": [
      {"id": "GS-AUTOMATED-DECISION", "severity": "critical",
       "label": "Automated decision with a significant effect",
       "detail": "accounts scoring below 0.3 are auto-downgraded"},
      {"id": "GS-CROSS-BORDER", "severity": "high",
       "label": "Personal data leaves a declared region", "detail": "EU -> US"}
    ],
    "gates": [{"gate": "DPIA", "role": "dpo"}]
  }
}

Response data.output.output, parsed

{
  "lane": "applicability",
  "lane_inferred": false,
  "initiative": "Weekly churn-risk email",
  "title": "Churn-risk scoring and alerting - applicable frameworks and gates",
  "posture": "conditions-first",
  "verdict": "The launch is satisfiable before 15 September, but the auto-downgrade makes it an Art. 22 decision and that changes what has to exist first.",
  "summary": "The programme scores every workspace nightly on personal data from three declared regions and acts on the score without a person in the loop ... This is a working analysis for privacy counsel and the accountable owner to review, and is not legal advice.",
  "jurisdictions": ["EU/EEA", "United Kingdom", "California"],
  "frameworks": [
    {"id": "GDPR", "name": "EU General Data Protection Regulation", "applies": "yes",
     "basis": "Art. 3(1) - processing personal data of EU data subjects",
     "why": "The facts block declares EU as a launch region and email and usage events as the data."},
    {"id": "CPRA", "name": "California Privacy Rights Act", "applies": "likely",
     "basis": "1798.140(v) - the business threshold is not stated in the input",
     "why": "California is a declared region, but no revenue or record-count fact was given."},
    {"id": "SOC2", "name": "SOC 2", "applies": "yes",
     "basis": "CC6.1 - logical access over the scoring pipeline",
     "why": "The facts block names SOC 2 as an in-force framework."},
    ...
  ],
  "assumptions": ["The score is computed from product usage events only; the brief does not mention any purchased data."],
  "open_questions": ["Is the 0.3 threshold reviewable by a human before the downgrade takes effect?"],
  "findings": [
    {"id": "LG-001", "title": "Treat the auto-downgrade as an Art. 22 decision",
     "severity": "critical", "area": "ai-governance",
     "requirement": "GDPR Art. 22(1) - no solely automated decision with legal or similarly significant effect without a lawful ground and safeguards",
     "evidence": "accounts scoring below 0.3 are auto-downgraded",
     "why": "A downgrade changes the service the customer receives, and no human review is described anywhere in the input.",
     "action": "Insert human review before any downgrade takes effect, or establish a lawful ground and the Art. 22(3) safeguards.",
     "owner_role": "privacy-counsel"},
    {"id": "LG-002", "title": "Establish the EU-to-US transfer basis before Segment is enabled",
     "severity": "high", "area": "cross-border",
     "requirement": "GDPR Chapter V - a transfer mechanism plus a transfer impact assessment",
     "evidence": "transfers: EU -> US; processors: Segment, SendGrid, Snowflake (US)",
     "why": "Three US processors receive EU personal data and only one DPA is signed.",
     "action": "Record the transfer mechanism for each US processor and countersign the Segment DPA.",
     "owner_role": "privacy-counsel"},
    ...
  ],
  "coverage_check": [
    {"flag_id": "GS-AUTOMATED-DECISION", "status": "confirmed", "finding_id": "LG-001", "note": "The downgrade is the significant effect."},
    {"flag_id": "GS-CROSS-BORDER", "status": "confirmed", "finding_id": "LG-002", "note": "Three named processors sit in the US."}
  ],
  "approvals": [
    {"role": "dpo", "decision_needed": "decide whether a DPIA is required and own it if so",
     "blocking": true, "by_when": "before launch"},
    {"role": "privacy-counsel", "decision_needed": "sign off the Art. 22 position and the transfer basis",
     "blocking": true, "by_when": "before launch"}
  ],
  "artifact": {"kind": "none", "filename": "", "content": ""},
  "next_lane": {"lane": "risk", "reason": "Four frameworks apply and two findings are high or above, so the exposure needs classifying before anyone sequences the work."},
  "body": {
    "in_scope": [
      {"framework_id": "GDPR", "obligation": "record the processing in the Art. 30 register",
       "trigger": "regions: EU with email, name, usage events and IP address",
       "provision": "Art. 30(1)", "when": "before launch"},
      {"framework_id": "GDPR", "obligation": "carry out a data protection impact assessment",
       "trigger": "automated-decision: yes, applied to every workspace",
       "provision": "Art. 35(3)(a)", "when": "before launch"},
      ...
    ],
    "out_of_scope": [
      {"framework_id": "HIPAA", "why": "No health data and no covered entity or business associate relationship appears in the input."}
    ],
    "data_map": [
      {"category": "email address", "special": false, "purpose": "addressing the churn alert",
       "lawful_basis_candidate": "legitimate interests (Art. 6(1)(f)), balancing test required",
       "retention_note": "24 months per the facts block"},
      ...
    ],
    "gates": [
      {"gate": "DPIA", "required": "yes", "role": "dpo",
       "lead_time_note": "Depends on the Art. 22 position being settled first; the input gives no internal turnaround time."},
      {"gate": "security review", "required": "yes", "role": "security",
       "lead_time_note": "Not started per the facts block, and it gates the SOC 2 control evidence."}
    ],
    "notice_changes": [
      "The privacy notice needs a line on churn scoring, on the consequence of a low score, and on the categories of US recipients."
    ],
    "scope_boundary": "This analysis covers privacy, cross-border transfer and audit-framework obligations on the facts given. It does not cover marketing-consent rules for the email channel itself, and it takes no view on the contractual position with the affected customers."
  }
}

task: "risk" — Classify and escalate

A severity-by-likelihood matrix over the risks the input raises, then the escalation criteria applied to it. severity uses the same four bands as findings; likelihood is one of rare, unlikely, possible, likely and almost-certain; and score_band is the product band, one of low, moderate, high and severe. The bands must be internally consistent — critical severity at likely likelihood cannot come out moderate — and that is worth asserting client-side. escalation.level is one of owner-decides, legal-review, senior-counsel, outside-counsel and executive-committee. Every row with escalate: true must appear in escalation.trigger_rows, and the matrix has at least three rows whenever any framework applies. artifact.kind is memo: a Markdown memo under 400 words that the owner can forward to the escalation target.

Request

{
  "task": "risk",
  "brief": "<the same launch brief>",
  "facts_text": "<the same facts block>",
  "carried": "applicability: GDPR, UK GDPR and SOC 2 apply; CPRA likely; DPIA and security review are the two blocking gates.",
  "prescan": {
    "signals": {"launch_in_days": 27, "automated_decision": true, "cross_border": true,
                "processors": [{"name": "Segment", "dpa_status": "pending"}]},
    "flags": [
      {"id": "GS-AUTOMATED-DECISION", "severity": "critical",
       "label": "Automated decision with a significant effect",
       "detail": "accounts scoring below 0.3 are auto-downgraded"},
      {"id": "GS-SECURITY-REVIEW-MISSING", "severity": "high",
       "label": "No security review has started", "detail": "security-review: not started"}
    ],
    "gates": [{"gate": "DPIA", "role": "dpo"}]
  }
}

Response data.output.output, parsed

{
  "lane": "risk",
  "lane_inferred": false,
  "initiative": "Weekly churn-risk email",
  "title": "Churn-risk scoring - risk classification and escalation",
  "posture": "conditions-first",
  "verdict": "One severe band survives the mitigations named in the input - the unreviewed automated downgrade - and it needs senior counsel rather than the owner.",
  "summary": "Six risks were classified ... The residual position is acceptable only if the downgrade gains a human step before 15 September. This is a working analysis for the accountable owner and senior counsel to review, and is not legal advice.",
  "jurisdictions": ["EU/EEA", "United Kingdom", "California"],
  "frameworks": [
    {"id": "GDPR", "name": "EU General Data Protection Regulation", "applies": "yes",
     "basis": "Art. 22 and Chapter V", "why": "Carried from the applicability lane and confirmed by the facts block."},
    ...
  ],
  "assumptions": ["The scope carried from the applicability run is taken as established and was not re-derived."],
  "open_questions": ["Is there any internal precedent for an automated service downgrade, and where is it recorded?"],
  "findings": [
    {"id": "LG-001", "title": "Put a human step in front of the downgrade",
     "severity": "critical", "area": "ai-governance",
     "requirement": "GDPR Art. 22(3) - the right to obtain human intervention",
     "evidence": "accounts scoring below 0.3 are auto-downgraded",
     "why": "The exposure cannot be remediated after the fact: once a customer is downgraded, the decision has already had its effect.",
     "action": "Hold downgrades in a review queue for the owner named in the facts block.",
     "owner_role": "Priya Raman, PM Growth"},
    ...
  ],
  "coverage_check": [
    {"flag_id": "GS-AUTOMATED-DECISION", "status": "confirmed", "finding_id": "LG-001", "note": "Row R1 in the matrix carries it."},
    {"flag_id": "GS-SECURITY-REVIEW-MISSING", "status": "confirmed", "finding_id": "LG-003", "note": "Row R4; it is the gating dependency for the SOC 2 evidence."}
  ],
  "approvals": [
    {"role": "senior-counsel", "decision_needed": "accept or reject the residual Art. 22 position",
     "blocking": true, "by_when": "before launch"}
  ],
  "artifact": {"kind": "memo", "filename": "risk-memo.md",
               "content": "# Churn-risk email - risk memo\n\n**For:** senior counsel\n**From:** Priya Raman, PM Growth\n\n| Row | Risk | Severity | Likelihood | Band | Residual |\n|---|---|---|---|---|---|\n| R1 | ... |\n"},
  "next_lane": {"lane": "readiness", "reason": "The mitigations now need owners, evidence and a status, which is the readiness register."},
  "body": {
    "matrix": [
      {"id": "R1", "risk": "A customer is downgraded by the model with no human step",
       "area": "ai-governance",
       "severity": "critical", "likelihood": "likely", "score_band": "severe",
       "exposure": "Regulatory exposure under Art. 22 that cannot be undone once the downgrade lands, plus a contractual argument from the affected customer.",
       "mitigation": "Nothing in the input mitigates this; the brief describes the downgrade as automatic.",
       "residual_band": "severe", "owner_role": "product-owner", "escalate": true},
      {"id": "R4", "risk": "The scoring pipeline ships without a security review",
       "area": "security",
       "severity": "high", "likelihood": "likely", "score_band": "high",
       "exposure": "A SOC 2 CC6.1 exception at the next audit window and no evidence of access control over a new data path.",
       "mitigation": "None stated; the facts block says the review has not started.",
       "residual_band": "high", "owner_role": "security", "escalate": true},
      ...
    ],
    "escalation": {"level": "senior-counsel",
      "reason": "A severe residual band survives every mitigation nameable from the input, and the exposure is unremediable after the fact.",
      "who": "senior-counsel", "trigger_rows": ["R1", "R4"]},
    "worst_case": "A cohort of EU customers is downgraded by the model in the first week and one complains to a supervisory authority. The complaint arrives with no DPIA, no human-review step and no transfer record for two of the three US processors.",
    "accepted_risks": [
      {"id": "R6", "risk": "IP address is retained for the full 24 months alongside the usage events",
       "accepted_by_role": "product-owner",
       "condition": "Valid only while the retention line stays at 24 months and the field stays out of the score itself."}
    ],
    "appetite_note": "For a growth experiment on existing customer data this sits above a normal appetite, and it is the automated action rather than the scoring that puts it there."
  }
}

task: "readiness" — Build the register

The producing lane. One row per requirement, mapped to a control, an evidence artifact, an owner role and a status — the row a SOC 2 or ISO 27001 auditor, or a GDPR accountability request, will actually ask for. status is one of ready, in-progress, missing and not-applicable, and it is taken from the input: a DPA the facts block calls signed is ready, one it calls pending is in-progress, and silence is missing — never ready on an assumption. readiness_score must equal the actual status counts in register, every blocking: true row that is missing must have a matching gaps entry, and the register has at least four rows whenever any framework applies. All three are cheap to assert and all three are real defects when they fail. artifact.kind is register: the same rows as a Markdown table and nothing else in the file.

Request

{
  "task": "readiness",
  "brief": "<the same launch brief>",
  "facts_text": "<the same facts block>",
  "carried": "risk: R1 severe and unmitigated (automated downgrade); R4 high (no security review); escalated to senior-counsel.",
  "prescan": {
    "signals": {"launch_in_days": 27,
                "processors": [{"name": "SendGrid", "dpa_status": "signed"},
                               {"name": "Segment", "dpa_status": "pending"}],
                "frameworks": [{"id": "SOC2", "name": "SOC 2", "trigger": "frameworks: SOC 2"}]},
    "flags": [
      {"id": "GS-DPA-PENDING", "severity": "medium",
       "label": "A named processor has no signed agreement", "detail": "Segment pending"},
      {"id": "GS-SECURITY-REVIEW-MISSING", "severity": "high",
       "label": "No security review has started", "detail": "security-review: not started"}
    ],
    "gates": [{"gate": "DPIA", "role": "dpo"}, {"gate": "security review", "role": "security"}]
  }
}

Response data.output.output, parsed

{
  "lane": "readiness",
  "lane_inferred": false,
  "initiative": "Weekly churn-risk email",
  "title": "Churn-risk scoring - audit-readiness register",
  "posture": "conditions-first",
  "verdict": "Thirteen requirements, five of them missing, and every missing row has an owner in the facts block except the DPIA.",
  "summary": "The register maps GDPR, SOC 2 and ISO 27001 obligations onto controls and evidence ... Four rows are ready today, all of them pre-existing controls rather than anything this launch built. This is a working analysis for the accountable owner and the audit function to review, and is not legal advice.",
  "jurisdictions": ["EU/EEA", "United Kingdom", "California"],
  "frameworks": [
    {"id": "SOC2", "name": "SOC 2", "applies": "yes", "basis": "CC6.1 and CC7.2",
     "why": "The facts block names SOC 2 and the launch adds a new nightly data path."},
    ...
  ],
  "assumptions": ["Evidence locations were taken from the input; where it is silent the row says so rather than naming a system."],
  "open_questions": ["Where is the DPIA template kept, and who countersigns it?"],
  "findings": [
    {"id": "LG-001", "title": "Open the DPIA before the security review",
     "severity": "critical", "area": "records",
     "requirement": "GDPR Art. 35(1) - a DPIA before processing likely to result in a high risk",
     "evidence": "automated-decision: yes - accounts scoring below 0.3 are auto-downgraded",
     "why": "It is the one blocking row with no owner named anywhere in the input, and the security review depends on its output.",
     "action": "Assign the DPIA to the DPO this week.",
     "owner_role": "dpo"},
    ...
  ],
  "coverage_check": [
    {"flag_id": "GS-DPA-PENDING", "status": "confirmed", "finding_id": "LG-002", "note": "REQ-01 is in-progress on the Segment agreement."},
    {"flag_id": "GS-SECURITY-REVIEW-MISSING", "status": "confirmed", "finding_id": "LG-003", "note": "REQ-05, missing and blocking."}
  ],
  "approvals": [
    {"role": "dpo", "decision_needed": "own and sign the DPIA", "blocking": true, "by_when": "before launch"},
    {"role": "security", "decision_needed": "complete the review and record the result", "blocking": true, "by_when": "before launch"},
    {"role": "procurement", "decision_needed": "countersign the Segment DPA", "blocking": true, "by_when": "before launch"}
  ],
  "artifact": {"kind": "register", "filename": "audit-readiness-register.md",
               "content": "| Req | Requirement | Framework | Provision | Control | Evidence | Owner | Status | Blocking | Verification |\n|---|---|---|---|---|---|---|---|---|---|\n| REQ-01 | signed Art. 28 processor agreement with Segment | GDPR | Art. 28(3) | ... |\n"},
  "next_lane": {"lane": "", "reason": "This is the last lane; what remains is closing the gaps with their owners."},
  "body": {
    "register": [
      {"req_id": "REQ-01", "requirement": "signed Art. 28 processor agreement with Segment",
       "framework_id": "GDPR", "provision": "Art. 28(3)",
       "control": "procurement countersigns the DPA before the vendor is enabled in production",
       "evidence_artifact": "countersigned DPA PDF in the contracts repository",
       "owner_role": "procurement", "status": "in-progress", "blocking": true,
       "verification": "An auditor pulls the vendor list and asks for a countersigned agreement per name."},
      {"req_id": "REQ-04", "requirement": "DPIA covering the churn score and the automated downgrade",
       "framework_id": "GDPR", "provision": "Art. 35(3)(a)",
       "control": "the DPO signs the DPIA before the scoring job runs against production data",
       "evidence_artifact": "the signed DPIA; the input does not say where DPIAs are kept",
       "owner_role": "dpo", "status": "missing", "blocking": true,
       "verification": "An auditor asks for the DPIA and its sign-off date, and compares it against the launch date."},
      {"req_id": "REQ-05", "requirement": "security review of the nightly scoring pipeline",
       "framework_id": "SOC2", "provision": "CC6.1",
       "control": "security reviews and records access control over each new data path",
       "evidence_artifact": "the review record; not started per the facts block",
       "owner_role": "security", "status": "missing", "blocking": true,
       "verification": "An auditor samples new data paths in the period and asks for the review for each."},
      ...
    ],
    "gaps": [
      {"req_id": "REQ-04", "gap": "No DPIA exists and no owner is named for it in the input",
       "blocking": true, "first_step": "Assign the DPIA to the DPO and open it against the model card legal has already seen.",
       "owner_role": "dpo"},
      {"req_id": "REQ-05", "gap": "The security review has not started", "blocking": true,
       "first_step": "Book the review for the scoring pipeline, not for the email channel.",
       "owner_role": "security"},
      ...
    ],
    "audit_pack": [
      {"item": "the DPIA", "where_it_lives": "the input does not say"},
      {"item": "the Art. 30 record for the churn processing", "where_it_lives": "the input does not say"}
    ],
    "cadence": [
      {"activity": "re-review the churn model's feature list", "frequency": "quarterly",
       "owner_role": "data-engineering", "why": "a new feature can pull in a data category the DPIA never covered"},
      {"activity": "re-check the downgrade review queue for backlog", "frequency": "monthly",
       "owner_role": "product-owner", "why": "a queue nobody empties is not a human step"}
    ],
    "readiness_score": {"ready": 4, "in_progress": 3, "missing": 5, "not_applicable": 1}
  }
}

readiness_score above sums to thirteen, the number of rows the register actually has. Check that yourself: it is the cheapest test of whether the register in front of you is the register the model counted.

The pipeline

The three lanes are meant to be run in order over the same work object, and each response says so in next_lane. The handoff is carried: a short digest of the previous lane's output, so the risk lane classifies inside the scope applicability drew instead of re-deriving it, and readiness builds the register against the risks that were actually escalated. carried is a convenience, not a channel — the brief and facts_text still go with every call, because each run is independent and the model has no memory of the last one.

Give each lane its own idempotency key. Same brief, different task: if the lane is not in the key, the risk run collides with the applicability run and hands back the applicability lane's cached result, with no error anywhere to say why the matrix is missing.

run_and_wait is step 5 wrapped in a function: post to /run with the idempotency key, poll GET /jobs/{job_id} until terminal, and return json.loads(status["output"]["output"]).

Notes that will save you a support round trip

What this app is not

Launch Gate produces a working analysis for a named human reviewer — privacy counsel, security, or the accountable owner in the facts block. It is not a clearance and it is not legal advice. posture is a recommendation to the approver, never a decision instead of one; a framework the input does not place goes into open_questions rather than being asserted, and an approval the input does not evidence is missing rather than assumed. Build your integration so the output lands in front of the reviewer, not in front of the launch button.

This is a working compliance analysis for a named human reviewer. It is not legal advice. Launch Gate is not your lawyer and does not form a lawyer-client relationship. An applicability call, a transfer basis, a DPIA conclusion, a risk acceptance and an audit position all need a qualified professional in your jurisdiction. Nothing here states a penalty, a fee or a statutory deadline, and no output should be relied on as clearance to launch.