doc_to_post chained two live actions — read a doc, post to LinkedIn — with no safety net. What happens to a workflow that chains three or four actions if step two raises an exception?
Everything after that point dies. The LinkedIn post never goes out, the email never sends. The whole function just crashes and you don't know where it stopped.
Exactly. And Composio has a specific exception you need to know: when an OAuth token expires mid-run, execute_action raises a RuntimeError whose message starts with AUTH_REQUIRED: followed by the app slug. Try to catch a generic Exception and you'll accidentally swallow real bugs you need to see. Check the prefix:
except RuntimeError as e:
if str(e).startswith("AUTH_REQUIRED:"):
return {"status": "reauth_required", "error": str(e)}
return {"status": "error", "error": str(e)}So I check the message string, not a subclass? How does the caller know whether to retry or just surface the error to the user?
The status key does that work. reauth_required tells the caller: stop the workflow, prompt the user to reconnect Gmail. error tells the caller: something else broke, surface it for debugging. Here's the full function:
def safe_send(to: str, subject: str, body: str) -> dict:
try:
result = toolset.execute_action(Action.GMAIL_SEND_EMAIL, {
"recipient_email": to, "subject": subject, "body": body,
})
return {"status": "sent", "result": result}
except RuntimeError as e:
if str(e).startswith("AUTH_REQUIRED:"):
return {"status": "reauth_required", "error": str(e)}
return {"status": "error", "error": str(e)}The function never raises. The caller just reads the status key and decides what to do. The workflow keeps moving even when Gmail needs re-auth.
You just described the whole discipline in one sentence. Every write action in a real workflow should follow this shape — return a status dict, never let an auth error crash an unattended run.
I could wrap doc_to_post the same way. If LinkedIn auth expires, the doc read still succeeds and the caller knows exactly why the post didn't go out.
That's composable error handling. Tomorrow's capstone chains three or more apps — you will need this pattern. A workflow that handles AUTH_REQUIRED gracefully runs unattended; one that doesn't will page you at 2am.
Composio raises RuntimeError with an AUTH_REQUIRED:<slug> prefix when an OAuth token has expired. Catching it separately from other errors lets you return actionable status instead of crashing.
| Scenario | str(e).startswith("AUTH_REQUIRED:") | Action |
|---|---|---|
| Token expired | True | Return {"status": "reauth_required"} — prompt user to reconnect |
| Bad parameters | False | Return {"status": "error"} — surface for debugging |
| Network failure | False | Return {"status": "error"} — surface for debugging |
Always return a dict with a status key. Callers check result["status"] instead of catching exceptions — workflows stay composable.
doc_to_post chained two live actions — read a doc, post to LinkedIn — with no safety net. What happens to a workflow that chains three or four actions if step two raises an exception?
Everything after that point dies. The LinkedIn post never goes out, the email never sends. The whole function just crashes and you don't know where it stopped.
Exactly. And Composio has a specific exception you need to know: when an OAuth token expires mid-run, execute_action raises a RuntimeError whose message starts with AUTH_REQUIRED: followed by the app slug. Try to catch a generic Exception and you'll accidentally swallow real bugs you need to see. Check the prefix:
except RuntimeError as e:
if str(e).startswith("AUTH_REQUIRED:"):
return {"status": "reauth_required", "error": str(e)}
return {"status": "error", "error": str(e)}So I check the message string, not a subclass? How does the caller know whether to retry or just surface the error to the user?
The status key does that work. reauth_required tells the caller: stop the workflow, prompt the user to reconnect Gmail. error tells the caller: something else broke, surface it for debugging. Here's the full function:
def safe_send(to: str, subject: str, body: str) -> dict:
try:
result = toolset.execute_action(Action.GMAIL_SEND_EMAIL, {
"recipient_email": to, "subject": subject, "body": body,
})
return {"status": "sent", "result": result}
except RuntimeError as e:
if str(e).startswith("AUTH_REQUIRED:"):
return {"status": "reauth_required", "error": str(e)}
return {"status": "error", "error": str(e)}The function never raises. The caller just reads the status key and decides what to do. The workflow keeps moving even when Gmail needs re-auth.
You just described the whole discipline in one sentence. Every write action in a real workflow should follow this shape — return a status dict, never let an auth error crash an unattended run.
I could wrap doc_to_post the same way. If LinkedIn auth expires, the doc read still succeeds and the caller knows exactly why the post didn't go out.
That's composable error handling. Tomorrow's capstone chains three or more apps — you will need this pattern. A workflow that handles AUTH_REQUIRED gracefully runs unattended; one that doesn't will page you at 2am.
Composio raises RuntimeError with an AUTH_REQUIRED:<slug> prefix when an OAuth token has expired. Catching it separately from other errors lets you return actionable status instead of crashing.
| Scenario | str(e).startswith("AUTH_REQUIRED:") | Action |
|---|---|---|
| Token expired | True | Return {"status": "reauth_required"} — prompt user to reconnect |
| Bad parameters | False | Return {"status": "error"} — surface for debugging |
| Network failure | False | Return {"status": "error"} — surface for debugging |
Always return a dict with a status key. Callers check result["status"] instead of catching exceptions — workflows stay composable.
Create a free account to get started. Paid plans unlock all tracks.