Problema

En muchos procesos de aprobación o verificación se necesita enviar una Adaptive Card a Teams y esperar una respuesta del usuario. La acción Post adaptive card and wait for a response permite definir un timeout (por ejemplo, PT2M). Cuando el usuario pulsa el botón, la salida contiene el submitActionId; cuando el timeout ocurre, la salida es nula y el estado de la acción pasa a TimedOut.

El desafío aparece al intentar encadenar una condición que evalúe ambos escenarios dentro de un bucle Until. Si la condición intenta leer body('Post_adaptive_card_and_wait_for_a_response')['submitActionId'] cuando la tarjeta expiró, el motor lanza InvalidTemplate porque la propiedad no existe. El resultado es un flujo que se detiene antes de poder enviar recordatorios o marcar la tarea como completada.

Causa

  1. Acceso directo a la carga del webhook: La expresión body('action') asume que siempre hay un objeto JSON. En caso de timeout, la acción devuelve {} y la evaluación falla.
  2. Orden de ejecución: La condición se ejecuta después de la acción, pero el motor solo considera los resultados de estados Succeeded y Failed por defecto. Cuando la acción termina en TimedOut, la condición necesita estar configurada para ejecutarse también en ese caso.
  3. Uso de actions('action')['status'] sin manejo de nulidad: Aunque actions('action')['status'] devuelve la cadena "TimedOut", la expresión suele combinarse con otras verificaciones que siguen intentando leer el cuerpo, provocando el mismo error.

Solución

La forma más robusta es separar claramente los dos caminos:

  1. Obtener el estado de la acción mediante actions('Post_adaptive_card_and_wait_for_a_response')['status'].
  2. Usar coalesce para extraer submitActionId solo cuando exista, evitando InvalidTemplate.
  3. Configurar runAfter de la condición para que acepte TimedOut como ruta válida.

Un patrón reutilizable queda así:

{
  "type": "If",
  "expression": {
    "or": [
      {
        "equals": [
          "@actions('Post_adaptive_card_and_wait_for_a_response')['status']",
          "Succeeded"
        ]
      },
      {
        "equals": [
          "@actions('Post_adaptive_card_and_wait_for_a_response')['status']",
          "TimedOut"
        ]
      }
    ]
  },
  "runAfter": {
    "Post_adaptive_card_and_wait_for_a_response": [
      "Succeeded",
      "TimedOut"
    ]
  },
  "actions": {
    "CheckResponse": {
      "type": "If",
      "expression": {
        "equals": [
          "@coalesce(body('Post_adaptive_card_and_wait_for_a_response')?['submitActionId'], '')",
          "completed"
        ]
      },
      "actions": {
        "SetTaskComplete": {
          "type": "SetVariable",
          "inputs": {
            "name": "TaskAComplete",
            "value": true
          }
        }
      },
      "else": {
        "SendReminder": {
          "type": "ApiConnection",
          "inputs": {
            "method": "post",
            "path": "/v2/Microsoft.Outlook/sendMail",
            "body": {
              "message": {
                "subject": "Recordatorio: completa la tarea",
                "toRecipients": [
                  {
                    "emailAddress": {
                      "address": "responsable@contoso.com"
                    }
                  }
                ]
              }
            }
          }
        }
      }
    }
  }
}

Puntos clave del fragmento:

  • La condición externa (If) solo verifica que la acción haya terminado, sea Succeeded o TimedOut.
  • Dentro, coalesce devuelve una cadena vacía cuando el cuerpo es nulo, evitando la excepción.
  • La comparación con "completed" (o el valor que corresponda al submitActionId esperado) determina si la tarea está concluida.
  • El bucle Until sigue evaluando @equals(variables('TaskAComplete'), true) y se detendrá cuando la variable se ponga a true.

Cuándo aplicar esta solución

  • Flujos que requieren interacción humana mediante Adaptive Cards y deben repetir recordatorios hasta obtener una respuesta.
  • Escenarios con timeout definido (cualquier valor de PTnM o PTnS).
  • Cuando el flujo se ejecuta en entornos de producción y no se puede permitir que un InvalidTemplate rompa la cadena de acciones.

No es necesario usar esta lógica si la tarjeta nunca expira (por ejemplo, se espera indefinidamente) o si la respuesta se gestiona mediante otro trigger (como When a HTTP request is received). En esos casos, la condición basada en submitActionId basta sin comprobar el estado.

Código

# Ejemplo de expresión para la condición interna
# Se coloca directamente en el campo "Expression" del bloque If
@equals(coalesce(body('Post_adaptive_card_and_wait_for_a_response')?['submitActionId'], ''), 'completed')

Verificación

  1. Desplegar el flujo con el bucle Until y la condición mostrada.
  2. Ejecutar una prueba enviando la Adaptive Card a Teams.
  3. No pulsar el botón y esperar al timeout (2 min).
    • Verificar en el historial que la acción Post_adaptive_card_and_wait_for_a_response tiene status = TimedOut.
    • Confirmar que la rama else (recordatorio) se ejecuta sin error.
  4. Volver a ejecutar y pulsar el botón antes del timeout.
    • El historial debe mostrar status = Succeeded y la rama then (SetTaskComplete) debe activarse.
  5. Comprobar la variable TaskAComplete al final del ciclo; debe ser true solo cuando se recibió la respuesta.

Si alguna de las verificaciones falla, revisar que el runAfter incluye TimedOut y que la expresión coalesce está escrita exactamente como se muestra.

Notas adicionales

  • En entornos con alta concurrencia, es buena práctica generar un correlationId y pasarlo dentro del value del botón. Así la condición puede validar que la respuesta pertenece al mismo ciclo del bucle.
  • La acción Post adaptive card and wait for a response no permite cambiar dinámicamente el timeout dentro del mismo ciclo; si se necesita un tiempo variable, recrear la acción con una variable que contenga la cadena PT${minutes}M.
  • Cuando se usan varios botones, el submitActionId será distinto para cada uno; la lógica de coalesce sigue funcionando, solo hay que comparar contra el identificador esperado.
  • Si el flujo se ejecuta bajo una suscripción con limite de acciones bajo, limitar el número de iteraciones del Until (propiedad limit.count) evita bloqueos inesperados.

Con este patrón, los flujos que dependen de Adaptive Cards pueden recuperarse de timeouts sin romper la cadena de ejecución, garantizando que los recordatorios se envíen y que la tarea se marque como completada tan pronto como el usuario interactúe.