Calling tools asynchronously

My agent has plenty of tools and uses other agent as tools, it takes quite a while to run... and when user switch page and come back, progress is gone.

Can we make tool calls asychronously?

Hi @wonka
This is a great question, and it highlights a common hurdle with building more complex, agentic workflows. You're right on both counts:

  1. Long-running agent runs can feel slow and unresponsive.
  2. The loss of progress when a user navigates away is a classic state management problem.

While the AI Agent's core execution loop is inherently synchronous, we can solve both of these issues by separating the agent's execution from the UI's display logic.

If i have understood your question correctly, here’s a two-part solution that addresses both issues.

Part 1: Make the Agent Run Asynchronously and Persist its State

The key is to move the heavy lifting of the agent's execution out of the user's browser session. The best way to do this is to call your AI Agent from a Retool Workflow.

How to implement this:

  1. Wrap your agent call in a Workflow: Create a new Retool Workflow. In this workflow, the first step will be to trigger your agent. You can pass the user's prompt and any other necessary data from your app to the workflow.
  2. Use a database to persist progress: The Workflow will be the "source of truth" for the agent's progress. Use a database (e.g., PostgreSQL, a temporary table, etc.) to store the agent's progress.
  • Create a table to track agent runs. It should include fields like run_id (a unique ID), status (e.g., "running," "completed," "failed"), input_prompt, and result (a JSON or text field to store the final output).
  1. Update the status in the Workflow: As the agent runs and completes its tasks, the Workflow can update the status and result fields in your database. For example, after the agent returns a result, the next step in the workflow would be a query to your database to update the status to "completed" and save the final output.
  2. Send a success signal: The Workflow can send an email, a Slack notification, or even an API call back to your app to signal that the process is complete.

This design solves the asynchronous problem. The user can close the app or navigate away, and the Workflow will continue running in the background.

Part 2: Display Asynchronous Progress in the UI

Now that the agent is running in the background, you need a way to show its progress to the user without a "loading" spinner that never ends.

How to implement this:

  1. Start the Workflow: When the user clicks the "Run Agent" button in your app, it should not directly trigger the agent. Instead, it should trigger a JavaScript query that calls your new Workflow via the startWorkflow query type. This query will immediately return a run_id.
  2. Poll the database for status updates: Use the returned run_id to periodically poll your database table for the status field.
  • Set up a query (e.g., query_check_status) that takes the run_id as an input and returns the current status from the database.
  • Use a Retool Timer component to automatically trigger this query_check_status every few seconds.
  1. Conditionally show progress:
  • If the status is "running," you can display a "Processing..." message or even show the latest partial output stored in the database.
  • If the status is "completed," hide the loading indicator and display the final result from the database.
  • If the status is "failed," show an error message.

Benefits of this approach:

  • Improved User Experience: The UI is responsive, and the user knows the process is running in the background.
  • State Persistence: Because the state is stored in your database, if the user leaves and comes back, the UI will simply resume polling for the run_id and pick up exactly where it left off.
  • Scalability: Workflows are designed for long-running, asynchronous tasks, so you won't hit timeouts or other browser-based limitations.
2 Likes

hehe I read this and was like "yup, that smrt" but then I re-read it a bit and went "WAIIITTTT A SECOND, that sir is very smart. :tophat:"... for a couple reasons other than what's already been stated by @turps808:

  1. Normally, I auto-generate things like run_id (old value for me from now the old OpenAI Assistants), message_id, conversation_id on the DB side. Which is nice, I don't have to remember to store/create anything but, that also means I have to make a follow-up read to get newly created ids like those if I want to use them. Now I can get a run_id, store it in my db and I still have it to use. yay.
  2. this polling system is smart too, there aren't many other ways to get info/data from a running workflow to the UI.... WHILE the workflow is still running. Actually, I can only think of one or 2 possible ways, by using Tasks, or MAYBE a Module that makes extensive use of it's Outputs. I think a Custom Component might actually be able to do this by sending out events, but we've now entered the 100% code solution, so ya, there's that.
  3. I think each workflow gets its own memory allocation for running (maybe Retool will correct me if not), which means by using a base workflow as both storage and for what controls things each workflow you call to run an Agent gets to make use of the full workflow memory space. Maybe I just have PTSD from my C/C++ days, but memory overflows suck to diagnose and fix.... it's worse when they lead to some other larger problem which is what gets reported as the error (like MemSegFault.... overflow a buffer into memory you shouldn't be in and now you get the angry OS Error :face_vomiting: instead of the polite Buffer Overflow Error

:innocent: little note: yes, I do realize Insert/Update on a SQL table can return the modified row (or parts of it). For my workflow with Agents (mine are pure Python right now) one of the code blocks passes 5.5k lines and it can be rather memory intensive. So when Ill make changes to the DB first, then let that long block run, then I'll grab any changes that were made to the DB earlier. The return value of workflow blocks is held in memory until the end of the run, so when that long block hits I want to make sure it has as much memory available as possible (any memory used by Code Blocks while running is freed up after the block ends and only the blocks return value (if any) is stored in memory