<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sonam Gupta</title>
    <description>The latest articles on DEV Community by Sonam Gupta (@sonam_gupta_dba00ec9473e5).</description>
    <link>https://dev.to/sonam_gupta_dba00ec9473e5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4158484%2F743f0f2c-050d-43ad-8a3a-73d94d8311bd.jpg</url>
      <title>DEV Community: Sonam Gupta</title>
      <link>https://dev.to/sonam_gupta_dba00ec9473e5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sonam_gupta_dba00ec9473e5"/>
    <language>en</language>
    <item>
      <title>Build a Durable Chargeback Review Workflow on Telnyx Edge</title>
      <dc:creator>Sonam Gupta</dc:creator>
      <pubDate>Fri, 02 Oct 2026 21:43:49 +0000</pubDate>
      <link>https://dev.to/sonam_gupta_dba00ec9473e5/build-a-durable-chargeback-review-workflow-on-telnyx-edge-579o</link>
      <guid>https://dev.to/sonam_gupta_dba00ec9473e5/build-a-durable-chargeback-review-workflow-on-telnyx-edge-579o</guid>
      <description>&lt;p&gt;A chargeback is not a single API request. It is a case that can remain open for days while evidence arrives, deadlines approach, and a reviewer decides what should happen next.&lt;/p&gt;

&lt;p&gt;That makes chargeback handling an interesting systems problem. The application needs to remember the case, wake up at the right time, evaluate evidence consistently, communicate with the customer, and preserve an audit trail even when the runtime restarts.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/chargeback-adjudication" rel="noopener noreferrer"&gt;&lt;code&gt;chargeback-adjudication&lt;/code&gt;&lt;/a&gt; TypeScript example models each dispute as a durable actor running on Telnyx Edge Compute.&lt;/p&gt;

&lt;p&gt;The sample combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One Stateful Actor per dispute&lt;/li&gt;
&lt;li&gt;Telnyx Decision Models for structured evidence evaluation&lt;/li&gt;
&lt;li&gt;Scheduled tasks for decision and response deadlines&lt;/li&gt;
&lt;li&gt;SQL for evidence, review queues, and an append-only audit ledger&lt;/li&gt;
&lt;li&gt;SMS for customer updates and reviewer escalation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository uses synthetic evidence and defaults to &lt;code&gt;DEMO_MODE=true&lt;/code&gt;, so SMS messages are logged instead of sent while you explore the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The central design choice is simple: &lt;strong&gt;the actor is the dispute case&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chargeback webhook
        |
        v
DisputeCase actor, keyed by disputeId
        |
        +-- SQL evidence and audit rows
        +-- Scheduled decision task
        +-- Telnyx Decision Models request
        +-- Policy and human-review routing
        +-- Customer or reviewer SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the actor is keyed by &lt;code&gt;disputeId&lt;/code&gt;, later evidence is routed back to the same durable case instead of being reconstructed from a stateless webhook request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a case from a webhook
&lt;/h2&gt;

&lt;p&gt;The Edge handler accepts a chargeback event at &lt;code&gt;POST /webhook/chargeback&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhook/chargeback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parseJson&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChargebackPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disputeId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disputeId, customer, and orderId required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DISPUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disputeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onChargeback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;idFromName()&lt;/code&gt; gives every dispute a stable actor identity. Inside &lt;code&gt;onChargeback()&lt;/code&gt;, the actor stores the case state, seeds the sample evidence tables, calculates the response window, and schedules a decision task with a stable ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decide&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`decide:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;disputeId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stable task IDs are useful when webhook delivery or application logic retries. Repeated work can converge on the same scheduled operation instead of creating a new timer every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate several questions in one Decision Models request
&lt;/h2&gt;

&lt;p&gt;The actor assembles the order, delivery record, contact history, and any newly submitted evidence into one shared state object.&lt;/p&gt;

&lt;p&gt;It sends that context to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v2/ai/typesafe/v1/systemone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One request asks three named questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;choice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rule on the chargeback.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;approve_rebate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Delivery evidence supports the customer's order.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;request_evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Evidence is inconclusive; more proof is needed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Evidence supports the merchant; deny the dispute.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;loseProb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rate the merchant's risk of losing.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Clearly likely to win&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Uncertain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Clearly likely to lose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;fraud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Does this look like a fraud attempt?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These question types serve different purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;choice&lt;/code&gt; selects one supplied category.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noul&lt;/code&gt; produces a signal between zero and one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;score&lt;/code&gt; returns an expected position on the ordered rubric.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Decision Models &lt;code&gt;score&lt;/code&gt; should not automatically be treated as a calibrated probability. With three rubric entries, the result falls along the rubric’s zero-to-two range and can be fractional.&lt;/p&gt;

&lt;p&gt;Production policy should be tested against representative, labeled cases rather than treating the result as certainty.&lt;/p&gt;

&lt;p&gt;The endpoint defaults to &lt;code&gt;telnyx/decision-flash&lt;/code&gt; when &lt;code&gt;model&lt;/code&gt; is omitted. You can also set the model explicitly based on the context requirements of your workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep model output separate from business policy
&lt;/h2&gt;

&lt;p&gt;The model produces structured evidence signals. Application code still owns the business action.&lt;/p&gt;

&lt;p&gt;The sample’s policy layer can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Approve a rebate and notify the customer&lt;/li&gt;
&lt;li&gt;Request additional evidence&lt;/li&gt;
&lt;li&gt;Deny the claim&lt;/li&gt;
&lt;li&gt;Route a suspected-fraud case to a human review queue
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;FRAUD_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO reviewQueue VALUES (?, ?, ?)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disputeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fraud_hold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendSms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;reviewerOnCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Review required for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;disputeId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold in this sample is illustrative. Before using a similar rule in production, evaluate it against your own data, add authorization and reviewer controls, and clearly define which outcomes may be automated.&lt;/p&gt;

&lt;p&gt;Decision Models help determine what deserves attention. They do not remove the human reviewer from high-risk decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-evaluate when new evidence arrives
&lt;/h2&gt;

&lt;p&gt;A dispute can change after its initial review.&lt;/p&gt;

&lt;p&gt;The second webhook route, &lt;code&gt;POST /webhook/inbound-message&lt;/code&gt;, sends an SMS or MMS response back to the existing actor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onNewEvidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assembleEvidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mediaUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;judgeWithDecisionModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;newEvidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendAudit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;re-evaluated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The case keeps its identity and previous state. New evidence becomes another event in the same lifecycle, and the resulting evaluation is appended to the ledger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve an audit trail in SQL
&lt;/h2&gt;

&lt;p&gt;Every material event is written as a new row rather than overwriting the previous decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO audit VALUES (?, ?, ?, ?)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;disputeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This append-only pattern makes it possible to reconstruct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What evidence the application had&lt;/li&gt;
&lt;li&gt;Which model output it received&lt;/li&gt;
&lt;li&gt;Which policy branch was selected&lt;/li&gt;
&lt;li&gt;When the state changed&lt;/li&gt;
&lt;li&gt;Whether new evidence caused a re-evaluation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a real financial or healthcare workflow, this example is only a starting point.&lt;/p&gt;

&lt;p&gt;Add authentication, webhook verification, encryption, retention controls, idempotency around external actions, reviewer permissions, monitoring, and a domain-specific compliance review before processing sensitive data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the sample
&lt;/h2&gt;

&lt;p&gt;Clone the examples repository and install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/chargeback-adjudication

npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample configuration includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TELNYX_API_KEY=your_telnyx_api_key_here
RESPONSE_DEADLINE_DAYS=7
REVIEWER_ONCALL_E164=+1555XXXXXXXX
DEMO_MODE=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authenticate the Edge CLI, run the smoke test, and deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnyx-edge auth api-key &lt;span class="nb"&gt;set&lt;/span&gt; &amp;lt;your_telnyx_api_key&amp;gt;
npx tsx smoke_test.ts
telnyx-edge ship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DEMO_MODE=true&lt;/code&gt; suppresses real SMS delivery and uses synthetic evidence. The deployed adjudication path still requires a Telnyx API key to call Decision Models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where else this pattern fits
&lt;/h2&gt;

&lt;p&gt;The reusable idea is larger than chargebacks: assign one durable actor to every long-running case, keep its evidence and history nearby, request structured model outputs, and let explicit application policy determine what happens next.&lt;/p&gt;

&lt;p&gt;The same architecture could support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Insurance claim review&lt;/li&gt;
&lt;li&gt;Returns and refund exceptions&lt;/li&gt;
&lt;li&gt;Account-verification cases&lt;/li&gt;
&lt;li&gt;Compliance investigations&lt;/li&gt;
&lt;li&gt;Support escalations with deadlines&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/chargeback-adjudication" rel="noopener noreferrer"&gt;Chargeback adjudication source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/inference/decision-models" rel="noopener noreferrer"&gt;Decision Models documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/api-reference/decision-models/evaluate-decision-models-typesafe-compatible" rel="noopener noreferrer"&gt;Decision Models API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/agent-sdk" rel="noopener noreferrer"&gt;Agent SDK documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/agent-sdk/sql" rel="noopener noreferrer"&gt;Agent SDK SQL documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/agent-sdk/scheduled-tasks" rel="noopener noreferrer"&gt;Agent SDK scheduled tasks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;Telnyx AI skills and toolkits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portal.telnyx.com/" rel="noopener noreferrer"&gt;Telnyx Portal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>serverless</category>
      <category>edgecompute</category>
    </item>
    <item>
      <title>Build Automated Post-Call QA Scoring with Telnyx Decision Models</title>
      <dc:creator>Sonam Gupta</dc:creator>
      <pubDate>Fri, 02 Oct 2026 20:36:24 +0000</pubDate>
      <link>https://dev.to/sonam_gupta_dba00ec9473e5/build-automated-post-call-qa-scoring-with-telnyx-decision-models-nh2</link>
      <guid>https://dev.to/sonam_gupta_dba00ec9473e5/build-automated-post-call-qa-scoring-with-telnyx-decision-models-nh2</guid>
      <description>&lt;p&gt;Support teams have a math problem.&lt;/p&gt;

&lt;p&gt;Thousands of customer calls happen every week, but supervisors can manually review only a small fraction of them. Even when a call is graded, that score often ends up isolated in a spreadsheet. It cannot tell you whether an agent had one difficult conversation or is developing a pattern.&lt;/p&gt;

&lt;p&gt;That creates two distinct engineering problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Coverage:&lt;/strong&gt; evaluate more completed calls without requiring someone to listen to each recording.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; preserve each agent's results long enough to identify trends and make better coaching decisions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The open-source &lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/post-call-qa-scoring" rel="noopener noreferrer"&gt;&lt;code&gt;post-call-qa-scoring&lt;/code&gt;&lt;/a&gt; example addresses both. It uses Telnyx Decision Models to grade transcripts and a durable &lt;code&gt;QAAgent&lt;/code&gt; actor to maintain the quality history for each support agent.&lt;/p&gt;

&lt;p&gt;The result is not an autonomous disciplinary system. It is a way to surface calls and trends that deserve human attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the application does
&lt;/h2&gt;

&lt;p&gt;When a support call ends, the application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;receives the transcript from a &lt;code&gt;call-conversation-ended&lt;/code&gt; webhook&lt;/li&gt;
&lt;li&gt;uses &lt;code&gt;transcription-saved&lt;/code&gt; as a fallback when the transcript is not embedded&lt;/li&gt;
&lt;li&gt;routes the transcript to a durable actor keyed by the support agent ID&lt;/li&gt;
&lt;li&gt;asks a Telnyx Decision Model for three structured grading signals&lt;/li&gt;
&lt;li&gt;stores the result in private per-actor SQL&lt;/li&gt;
&lt;li&gt;recomputes a five-call rolling average&lt;/li&gt;
&lt;li&gt;flags or clears a coaching recommendation based on that trend&lt;/li&gt;
&lt;li&gt;escalates potential compliance breaches to a manager over SMS&lt;/li&gt;
&lt;li&gt;sends the team lead a daily per-agent digest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each call ID is stored as a SQL primary key before grading is scheduled. The scheduled task also uses a stable ID, &lt;code&gt;grade:&amp;lt;callId&amp;gt;&lt;/code&gt;, so webhook retries do not create duplicate grades.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use one actor per support agent?
&lt;/h2&gt;

&lt;p&gt;The central design decision is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The actor is the agent's quality profile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The worker resolves an actor with &lt;code&gt;idFromName(agentId)&lt;/code&gt;. Every later transcript for that support agent reaches the same durable actor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;QA_AGENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qaAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;QA_AGENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;qaAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordCallEnded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;agentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;digestEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That actor owns the score history, rolling average, coaching state, breach records, and digest schedule. The application does not have to reconstruct the agent's history from a cache every time another call ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grade the transcript into structured fields
&lt;/h2&gt;

&lt;p&gt;Free-text model output is awkward for QA automation. If the model returns a paragraph, downstream code has to interpret that prose before it can calculate an average or apply an escalation threshold.&lt;/p&gt;

&lt;p&gt;The example instead calls the Telnyx Decision Models endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v2/ai/typesafe/v1/systemone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One request evaluates the transcript against three question types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;choice&lt;/code&gt;: pass or a specific failure category&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noul&lt;/code&gt;: a 0–1 compliance-breach signal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;score&lt;/code&gt;: an overall quality score from 0–5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application can then apply ordinary program logic to typed values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DecisionResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A call can pass its general quality evaluation and still carry a serious compliance signal. The sample therefore treats these fields independently. When &lt;code&gt;noul &amp;gt; 0.8&lt;/code&gt;, it records the call in the &lt;code&gt;breaches&lt;/code&gt; table and sends an immediate manager-review alert regardless of the pass/fail choice.&lt;/p&gt;

&lt;p&gt;Decision Models do not replace the reviewer here. They decide which evidence should be placed in front of one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn individual grades into a trend
&lt;/h2&gt;

&lt;p&gt;After every successful grade, the actor queries its SQL history and recomputes a five-call rolling average.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'graded'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the average falls below &lt;code&gt;QA_COACHING_FLOOR&lt;/code&gt;, the actor flags the agent for coaching. If later calls bring the average back above the threshold, it clears the flag automatically.&lt;/p&gt;

&lt;p&gt;That distinction matters. One low score may be an isolated call. A declining rolling average is a stronger reason to investigate, coach, or review the underlying conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep failures visible
&lt;/h2&gt;

&lt;p&gt;The grading task uses bounded retries rather than retrying forever. Transient failures back off for 10, 30, and 60 seconds. If grading still cannot complete, the call is stored with &lt;code&gt;status="ungraded"&lt;/code&gt; and a &lt;code&gt;last_error&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;The daily digest can therefore distinguish between a low score and a transcript that was never successfully graded. Failed AI work does not quietly disappear from the reporting pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the complete pipeline without placing a call
&lt;/h2&gt;

&lt;p&gt;The repository includes a synthetic demo endpoint. After deploying the example, call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://&amp;lt;edge-function-url&amp;gt;/demo/trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "agentId": "demo-agent",
    "callId": "demo-001",
    "transcript": "Agent: This call may be recorded for quality purposes..."
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint routes the transcript through the same actor, Decision Models request, SQL history, trend calculation, and escalation logic used by the webhook flow. It is intended for demonstration and testing, so you can inspect the architecture before connecting a live call application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/team-telnyx/telnyx-code-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;telnyx-code-examples/post-call-qa-scoring

npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
npm run typecheck
npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For live Decision Models calls and SMS notifications, configure the Telnyx API key, a messaging-enabled Telnyx number, and the team lead's E.164 number as Edge secrets. The repository README documents every supported setting, including the coaching floor and digest hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to take this pattern next
&lt;/h2&gt;

&lt;p&gt;The sample uses a regional-bank support scenario, but the architecture is useful anywhere conversations need consistent review over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contact-center coaching&lt;/li&gt;
&lt;li&gt;healthcare communication audits&lt;/li&gt;
&lt;li&gt;insurance disclosure checks&lt;/li&gt;
&lt;li&gt;sales-call quality monitoring&lt;/li&gt;
&lt;li&gt;customer-service escalation review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important pattern is broader than call scoring: produce structured decisions, keep durable history around the entity being measured, and route high-stakes outcomes to a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/telnyx-code-examples/tree/main/post-call-qa-scoring" rel="noopener noreferrer"&gt;Post-call QA scoring code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/inference/decision-models" rel="noopener noreferrer"&gt;Telnyx Decision Models documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/agent-sdk" rel="noopener noreferrer"&gt;Telnyx Agent SDK documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/team-telnyx/ai" rel="noopener noreferrer"&gt;Telnyx AI skills and toolkits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com" rel="noopener noreferrer"&gt;Telnyx Developer documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>serverless</category>
      <category>telnyx</category>
    </item>
  </channel>
</rss>
