name: hospital-devolucion-response-sender
description: Sends the IPS's finished glosa responses back to an EPS via Gmail using gogcli, as a single consolidated Excel attachment with one row per glosa showing both the EPS objection and the IPS response side by side. Recipient resolved from the sender task context, formal Spanish body referencing the Res. 3047/2008 / Res. 416/2009 response window, delivery recorded on the sender task and on each glosa task. Use it once a human auditor triggers "Enviar" on a group of finished glosa responses for one pagador.
version: 1.0.0
author: claudio@arkangel.ai
platforms: [macos, linux]
metadata:
hermes:
tags: [medical, audit, gmail, glosa, devolucion, hospital, ips, eps, sender, colombia]
category: medical-insurance-audit
requires_toolsets: [terminal]
required_environment_variables:
- name: GOGCLI_CREDENTIALS_PATH prompt: Path to gogcli OAuth credentials help: The account needs gmail.send scope. Same credentials as hospital-devolucion-gmail-intake if sharing a single inbox. required_for: full functionality
- name: GMAIL_SENDER_ADDRESS prompt: Address the glosa response is sent from (e.g. glosas@ipsabc.com) required_for: full functionality
- name: SALMONA_API_URL prompt: Base URL of the Salmona API (e.g. https://salmona.arkangel.ai) required_for: full functionality
- name: SALMONA_API_KEY prompt: Salmona API key with agent role help: Must be able to read tasks, read task outputs, PATCH task context, and upload task outputs. required_for: full functionality
hospital-devolucion-response-sender
Last skill in the hospital devolucion pipeline. It takes the finished glosa responses (each hospital-devolucion-audit produced a glosa-response.json), consolidates them into one Excel file with one row per glosa, and sends it back to the EPS via Gmail. The Excel presents the EPS objection and the IPS response side by side so the payer can reconcile the response against its original glosa file.
The question it answers: how do I formally deliver the IPS's consolidated response to a pagador's glosas with traceability across every glosa task?
Flow position: hospital-devolucion-audit × N → human auditor clicks "Enviar" in Salmona → hospital_devolucion_response_mail (Salmona) → this skill → EPS inbox
Distinction from medical-invoice-claim-denial-gmail-sender: that skill is the EPS-side sender — it delivers a single glosa as a formal PDF to an IPS. This skill is the IPS-side sender — it delivers many glosa responses as a single Excel to an EPS. The direction, attachment format, and task model are different.
When to Use
- The orchestrator dispatches a task of type
hospital_devolucion_response_mail. - A human auditor in Salmona triggered "Enviar" on a group of finished glosa responses belonging to one pagador (EPS).
- Manual retry after a send failure, only if
context.reply_sentis stillfalseon the sender task.
Do not use:
- If
task_type != hospital_devolucion_response_mail. - If
context.reply_sentis alreadytrueon the sender task (avoid double-send — confirm with the user). - If
context.caso_idsis empty orcontext.email_destinois missing.
Input Contract
Required environment variables
All four are declared in the frontmatter so a runtime that provisions on required_environment_variables launches the skill with everything it needs:
GOGCLI_CREDENTIALS_PATH— gogcli OAuth credentials withgmail.sendscope.GMAIL_SENDER_ADDRESS— the address the response is sent from.SALMONA_API_URL— Base URL of the Salmona API (e.g.https://salmona.arkangel.ai).SALMONA_API_KEY— Salmona API key with agent role. Must be able to read tasks, read task outputs, PATCH task context, and upload task outputs.
Task context shape
Task of type hospital_devolucion_response_mail. Its context is the sender envelope — metadata about which glosa responses to bundle, not glosa data itself. The Salmona route (arkangelai/salmona-api#210) groups by pagador, so one sender task = one EPS:
{
"caso_ids": ["HOSP-GL-1052", "HOSP-GL-1055"],
"pagador_nombre": "EPS Sura",
"pagador_nit": "800.088.702-2",
"email_destino": "glosas@epssura.com",
"solicitado_por": "<user-id>",
"reply_sent": false,
"sent_at": null,
"excel_filename": null,
"total_glosas_respondidas": null
}
Each entry in caso_ids is normally a GlosaContext.caso_id (e.g. HOSP-GL-20260514-A1B2C), but the orchestrator may instead hand a raw hospital_devolucion task UUID. Procedure step 2 resolves both forms.
reply_sent, sent_at, excel_filename and total_glosas_respondidas start null/false and are written back by this skill.
Output Contract
On success, the skill:
- Sends one email with one
.xlsxattachment tocontext.email_destino. - Sets
context.reply_sent = trueon eachhospital_devolucionglosa task. - Updates the sender task
contextwithreply_sent,sent_at,excel_filename,total_glosas_respondidas. - Uploads the generated
.xlsxas the sender task's output with labelreport.
The skill returns a JSON summary block:
{
"pagador_nombre": "EPS Sura",
"pagador_nit": "800.088.702-2",
"email_destino": "glosas@epssura.com",
"excel_filename": "Respuestas_Glosas_8000887022_20260514.xlsx",
"total_glosas_respondidas": 12,
"sent_message_id": "<gmail-message-id>",
"thread_id": "<gmail-thread-id>",
"sent_at": "2026-05-14T16:20:00-05:00"
}
Procedure
Pre-flight checks.
gogcli --version test -n "$GMAIL_SENDER_ADDRESS" || { echo "Missing GMAIL_SENDER_ADDRESS"; exit 1; } test -f "$GOGCLI_CREDENTIALS_PATH" || { echo "Missing gogcli creds"; exit 1; }Read the sender task
context. Abort with a clear message if:context.reply_sentis alreadytrue(already sent — do not resend).context.caso_idsis empty or absent.context.email_destinois missing.
Resolve each glosa task and its response.
Each entry in
context.caso_idsis either aGlosaContext.caso_idor a rawhospital_devoluciontask UUID — resolve both forms:- UUID → fetch the task directly, no search needed:
curl -s -H "Authorization: Bearer $SALMONA_API_KEY" "$SALMONA_API_URL/api/tasks/$VALUE" - caso_id → find the matching
hospital_devoluciontask./api/taskscapslimitat 100 and paginates by cursor, so a single page is not enough — followmeta.next_cursoruntil it is null, collecting every task, then match bycontext.caso_id:CURSOR=""; ALL='[]' while :; do PAGE=$(curl -s -H "Authorization: Bearer $SALMONA_API_KEY" \ "$SALMONA_API_URL/api/tasks?task_type=hospital_devolucion&limit=100${CURSOR:+&cursor=$CURSOR}") ALL=$(jq -s '.[0] + .[1].data' <<<"$ALL"$'\n'"$PAGE") CURSOR=$(jq -r '.meta.next_cursor // empty' <<<"$PAGE") [ -z "$CURSOR" ] && break done jq --arg cid "$CASO_ID" '.[] | select(.context.caso_id == $cid)' <<<"$ALL"
A
caso_id(or task UUID) that is never resolved is recorded as missing in the run log — it must not silently shrink the batch toward a zero-row send (see step 3).From each resolved task read:
context— theGlosaContext(see../hospital-devolucion-audit/references/glosa-context-template.json). Source of the EPS-side columns.- The
reportoutput — theGlosaResponse(glosa-response.json, see../hospital-devolucion-audit/references/glosa-response-template.json):curl -s -H "Authorization: Bearer $SALMONA_API_KEY" \ "$SALMONA_API_URL/api/tasks/$CASO_TASK_ID/outputs?label=report"
If a caso has no
reportoutput, skip it: log thecaso_idand the reason, and continue with the rest of the batch. Do not abort the whole send.Validate
pagador_nitconsistency: every matchedGlosaContextshould carry the samepagador_nitascontext.pagador_nit. If any caso differs, warn (the route groups by pagador, so this should not happen) and exclude the mismatched caso.- UUID → fetch the task directly, no search needed:
Generate the consolidated Excel.
Genere un archivo
.xlsxcon una sola hoja llamadaRespuestas, una fila por glosa, en este orden EXACTO de columnas. No usar librerías de parsing para leer — esto es generación de salida; produzca el.xlsxcon las capacidades disponibles en el host (p. ej.openpyxl), de forma tool-agnóstica como el resto del pipeline.# Columna Origen 1 CASO_IDGlosaContext.caso_id2 NUM_FACTURAGlosaContext.num_factura_original3 PACIENTEGlosaContext.paciente_alias4 CODIGOGlosaContext.codigo5 DESCRIPCIONGlosaContext.descripcion6 CANTIDADGlosaContext.cantidad7 CAUSALGlosaContext.causal_num+" — "+ nombre de la causal (Res. 3047/2008 Anexo 6)8 MOTIVO_GLOSA_EPSGlosaContext.motivo_glosa(texto literal de la objeción de la EPS)9 VALOR_FACTURADOGlosaContext.valor_facturado(número, sin símbolo de moneda)10 VALOR_GLOSADOGlosaContext.valor_glosado(número)11 RESPUESTA_IPSGlosaResponse.decisionmapeado (ver abajo)12 VALOR_A_DEFENDERGlosaResponse.valor_a_defender(número)13 VALOR_A_ACEPTARGlosaResponse.valor_a_aceptar(número)14 ARGUMENTACIONGlosaResponse.argumentacion(texto apto para carta; vacío si pending)15 ESTADO_EVIDENCIAGlosaResponse.evidence_statusmapeado (ver abajo)16 EVIDENCIA_REQUERIDAGlosaResponse.evidencia_requeridaunida con"; "(vacío si ausente)Nombres de causal (columna
CAUSAL, porcausal_num):causal_numNombre 1Facturación 2Tarifas 3Soportes 4Autorización 5Cobertura 6Pertinencia 7Anulaciones Mapeo de
RESPUESTA_IPS(columna 11):GlosaResponse.decision == "disputar"→"Disputar"GlosaResponse.decision == "aceptar"→"Aceptar"GlosaResponse.decision == nulloevidence_status == "pending"→"Pendiente de soportes"
Mapeo de
ESTADO_EVIDENCIA(columna 15):"sufficient"→"Suficiente""pending"→"Pendiente"
Glosas con
decision: null/evidence_status: pendingigual van en el Excel, con la fila "Pendiente de soportes" yEVIDENCIA_REQUERIDApoblada. No omitirlas.Filename:
Respuestas_Glosas_{pagador_nit sanitizado}_{YYYYMMDD}.xlsx, donde el NIT se sanitiza quitando puntos y guiones (800.088.702-2→8000887022) y la fecha se calcula enAmerica/Bogota:DATE=$(TZ=America/Bogota date +%Y%m%d) NIT_SANITIZED=$(echo "$PAGADOR_NIT" | tr -dc '0-9') EXCEL_NAME="Respuestas_Glosas_${NIT_SANITIZED}_${DATE}.xlsx"Record the final row count — it becomes
total_glosas_respondidas.Abort if the row count is 0. If every caso was skipped (missing
reportoutputs, pagador mismatches), there is nothing to send. Do not compose or send an email, and do not setreply_sent. Fail the sender task with a clear message listing the skippedcaso_ids so a human can investigate — sending a "0 glosas" email and marking the batch sent would permanently hide the unanswered glosas.Compose and send the email via
gogcli.Subject:
Respuesta a glosas — {pagador_nombre} — {N} glosaswhere
Nis the row count.Body (formal Spanish — the recipient is a Colombian EPS):
Estimados, En el marco de la Resolución 3047 de 2008 y la Resolución 416 de 2009, {prestador_nombre} remite la respuesta consolidada a las glosas notificadas por {pagador_nombre}, dentro del plazo de respuesta establecido. Se adjunta un archivo Excel con {N} glosas. Cada fila presenta la objeción original de la EPS junto con la respuesta de la IPS: la decisión (disputar, aceptar o pendiente de soportes), el valor a defender, el valor a aceptar y la argumentación correspondiente. Quedamos atentos a la conciliación de las glosas disputadas y a cualquier requerimiento adicional de soportes. Atentamente, Equipo de Cuentas Médicas — {prestador_nombre}The user-facing email body and the Excel headers stay in Spanish because the recipient is a Colombian EPS — legal language and habit require it. Internal prose and comments stay in English.
Send:
gogcli messages send \ --from "$GMAIL_SENDER_ADDRESS" \ --to "$EMAIL_DESTINO" \ --subject "Respuesta a glosas — ${PAGADOR_NOMBRE} — ${N} glosas" \ --body-text body.txt \ --attach "$WORK_DIR/$EXCEL_NAME" \ --attach-mime "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"Capture from the response:
sent_message_id,thread_id.Record delivery on every glosa task.
For each
hospital_devoluciontask that was included in the Excel, setcontext.reply_sent = true, preserving the rest of the existing context:UPDATED_CTX=$(echo "$EXISTING_CONTEXT" | jq '.reply_sent = true') curl -s -X PATCH \ -H "Authorization: Bearer $SALMONA_API_KEY" \ -H "Content-Type: application/json" \ -d "$(jq -n --argjson context "$UPDATED_CTX" '{context: $context}')" \ "$SALMONA_API_URL/api/tasks/$CASO_TASK_ID"Update the sender task context.
PATCH the
hospital_devolucion_response_mailtask — merge into its existing context:SENDER_CTX=$(echo "$SENDER_CONTEXT" | jq \ --arg sent_at "$SENT_AT" \ --arg fn "$EXCEL_NAME" \ --argjson n "$N" \ '.reply_sent = true | .sent_at = $sent_at | .excel_filename = $fn | .total_glosas_respondidas = $n') curl -s -X PATCH \ -H "Authorization: Bearer $SALMONA_API_KEY" \ -H "Content-Type: application/json" \ -d "$(jq -n --argjson context "$SENDER_CTX" '{context: $context}')" \ "$SALMONA_API_URL/api/tasks/$SENDER_TASK_ID"sent_atis an ISO-8601 timestamp inAmerica/Bogota(-05:00).Upload the Excel as the sender task's output.
curl -s -X POST \ -H "Authorization: Bearer $SALMONA_API_KEY" \ -F "file=@$WORK_DIR/$EXCEL_NAME" \ -F "label=report" \ -F "description=$EXCEL_NAME" \ "$SALMONA_API_URL/api/tasks/$SENDER_TASK_ID/outputs/upload"Return the summary (see Output Contract).
Pitfalls
- Symptom: the whole batch aborts because one caso has no response. Cause: a
hospital_devoluciontask still inreview/pendinghas noreportoutput. Fix: skip that caso, log it, keep going — never let one missing response block the send for the others. - Symptom: the EPS receives a "0 glosas" email and the batch is marked sent, hiding unanswered glosas. Cause: every caso was skipped, but the skill still sent and set
reply_sent. Fix: the row-count check at the end of step 3 aborts the task when the count is 0 — nothing is sent andreply_sentstays false. - Symptom: a pending glosa is left out of the Excel. Cause: filtering out
decision: null. Fix: pending glosas still go in the Excel withRESPUESTA_IPS = "Pendiente de soportes"andEVIDENCIA_REQUERIDApopulated — the EPS needs to see them. - Symptom: the EPS receives two emails for the same group. Cause: retry without checking
context.reply_senton the sender task. Fix: the first pre-flight step aborts ifreply_sentis alreadytrue. - Symptom: the Excel mixes glosas from two different EPSs. Cause: the sender task context was built wrong. Fix: the route groups by pagador so this should not happen, but validate
pagador_nitconsistency across all matchedGlosaContexts (step 2) and exclude + warn on any mismatch. - Symptom:
gogclifails withinvalid_grant. Cause: OAuth token expired. Fix: rungogcli auth refreshor regenerate withgogcli auth init. - Symptom: the EPS opens the attachment and it is unreadable. Cause: wrong content-type on the attachment. Fix: always pass
--attach-mime application/vnd.openxmlformats-officedocument.spreadsheetml.sheet. - Symptom: inconsistent state after partial failure (email sent, task context not updated). Cause: error between step 4 and step 6. Fix: if the send succeeded but a Salmona PATCH failed, retry the PATCH with backoff — do NOT resend the email.
- Symptom:
América/Bogotafilename uses UTC date and is off by a day near midnight. Cause:datewithoutTZ. Fix: alwaysTZ=America/Bogota date +%Y%m%d.
Verification
- The sender task
contexthasreply_sent: true, a non-nullsent_at, and a non-nullexcel_filename. -
context.total_glosas_respondidasequals the number of rows in the Excel. - Every
hospital_devolucionglosa task included in the Excel hascontext.reply_sent: true. - The generated
.xlsxexists as the sender task's output with labelreport. - In Gmail, the message exists in
SENTwith the Excel attached and the subjectRespuesta a glosas — {pagador} — {N} glosas. - Any caso skipped for missing a
reportoutput is logged and absent from both the Excel and the row count.
References
../medical-invoice-claim-denial-gmail-sender/SKILL.md— EPS-side analog: delivers a single glosa as a PDF to an IPS.../hospital-devolucion-audit/SKILL.md— upstream: produces theglosa-response.jsonper glosa that this skill bundles.../hospital-devolucion-batch-parse/SKILL.md— upstream: splits the EPS Excel into thehospital_devoluciontasks.../hospital-devolucion-gmail-intake/SKILL.md— pipeline entry point.- Resolución 3047/2008 + Resolución 416/2009 — Manual Único de Glosas, Devoluciones y Respuestas. Causales 1–7 (Anexo 6) y plazos de respuesta.
gogcli— internal Arkangel CLI for Gmail (see#ai-toolingon Slack).- Issue
arkangelai/salmona-api#210— single-glosa data model; skills repo is the canonical home for agent prompts.