How to Solve CAPTCHAs in Retool Workflows (REST Blocks)

A Retool Workflows captcha solve is three blocks: submit, wait, read. Build them as REST resource query blocks rather than as a JavaScript or Python code block, because Retool runs code blocks in a separate sandboxed service whose default firewall rules refuse private addresses. A resource query is configuration rather than custom code, so it does not go through that service, and it reaches a solver on your own network without any of that argument.
What you need
- A Retool organization with Workflows, on Retool Cloud or self-hosted. Both work, with different network setup.
- CapSkip running on a Windows machine. Local mode if that machine also runs a self-hosted Retool, Server mode in every other case.
- The sitekey and the page URL of the challenge you are solving.
- Somewhere to put the API key. Retool secrets are reachable from code blocks and from resource configuration, so nothing needs to be pasted into a block.
CapSkip speaks the 2captcha-compatible API on port 8080, so Retool needs no connector and no custom integration. It is an ordinary REST resource pointed at a machine you own.
Step 1: point a REST resource at the solver
Create a REST API resource with the base URL of the machine running CapSkip. Leave authentication empty. The API key travels as an ordinary parameter on each request, which is how the 2captcha-compatible protocol works.
# Base URL for the resource. Loopback only works when Retool # is self-hosted on the same Windows box as the solver. http://127.0.0.1:8080 # Server mode, which is what you want everywhere else. http://192.168.1.40:8080
Every workflow that solves anything then reuses this one resource. Two query blocks are enough for the whole job.
Step 2: submit the challenge
Add a resource query block, name it submitCaptcha, set the action type to POST and the path to the submit endpoint. The body is a small JSON object.
{
"key": "YOUR_KEY",
"method": "userrecaptcha",
"googlekey": "YOUR_SITEKEY",
"pageurl": "https://example.com/page-with-recaptcha",
"json": 1
}The response carries the id you poll with.
{ "status": 1, "request": "2122988149" }That body is reCAPTCHA v2. The other types CapSkip supports are the same call with different parameters: add invisible or enterprise set to 1, or version set to v3 with an action name, or switch the method to turnstile or geetest. The full parameter list is in the CapSkip API documentation.
A resource query block hands three properties to everything downstream. The response body arrives as data, a failure message arrives as error, and metadata carries the rest. So the id you just collected is available to the next block as submitCaptcha.data.request.
Step 3: wait, then read the token once
Add a Wait block. Fifteen seconds is a sensible first wait for a reCAPTCHA v2 checkbox. Image CAPTCHAs come back in about a second, v3 in ten to fifteen, GeeTest in about five. A Wait block takes a number or a JavaScript expression and can be configured in seconds, minutes, hours or days, up to a ceiling of sixty days, and it pauses only the blocks directly downstream of it.
Then add a second resource query block called readResult, set to GET.
# GET, with the id from step 2 in the query string.
/res.php?key=YOUR_KEY&action=get&id={{ submitCaptcha.data.request }}&json=1Two answers are possible. A ready result has status set to 1 and the token in the request field. A result that is still working has status set to 0 and the string CAPCHA_NOT_READY in the request field, spelled without the T, and that means keep waiting rather than something went wrong. The history of that spelling is in the full write-up of the CAPCHA_NOT_READY response.
Handle the two cases with a Branch block. Conditions are plain JavaScript against an upstream block, so the test reads readResult.data.status === 1. The If path carries the token onward. The Else path gets a second Wait of twenty seconds and a second read.
Resist the urge to replace that with a Loop block. Two reasons, and the second is the one that actually bites. A Loop block has a default timeout of ten seconds and a ceiling of two minutes, which is well under the three hundred seconds CapSkip allows a reCAPTCHA solve, so a loop cannot cover the slow tail anyway. More importantly, a CapSkip result is readable once. A loop that re-reads an id it has already collected does not get the token a second time.
Step 4: the network rule that decides everything
This is the Retool-specific part, and it is why this guide builds the solve out of resource query blocks.
Retool runs your JavaScript and Python in a separate code executor service, sandboxed with NsJail. On a self-hosted deployment that service ships with iptables rules covering link local addresses and the whole of 192.168.0.0/16, and Retool documents a single switch that turns them off, DISABLE_IPTABLES_SECURITY_CONFIGURATION. Retool also states plainly that it recommends running the code executor privileged so custom code stays sandboxed. So a code block reaching for a solver at 192.168.1.40 is asking you to weaken the sandbox for the whole instance. A resource query is not custom code and does not run there.
Separately from that, the solver has to be reachable at all. CapSkip has two connection modes for this. Local binds to 127.0.0.1 and serves that device only. Server binds to your network address or public IP, so another box, a container host or a hosted platform can reach the same Windows machine over the API. Both live under connection settings, and Server mode changes only which address the solver listens on. It is still your hardware and it is still unmetered.
| Where Retool runs | Which mode, and what else |
|---|---|
| Self-hosted on the same Windows machine as CapSkip | Local mode. The base URL stays on loopback |
| Self-hosted on another box or in Docker on your network | Server mode with the solver’s LAN address. Use a resource query block, not a code block |
| Retool Cloud | Server mode with a static public IP, plus an inbound firewall rule for Retool’s outbound addresses |
Retool Cloud calls your resources from a fixed, published set of addresses, and the documentation says cloud instances must ensure configured resources allow access from them. The default region is AWS us-west-2.
# Retool Cloud outbound ranges, us-west-2, the default region. 35.90.103.132/30 44.208.168.68/30 # eu-central-1 3.77.79.248/30
Allow those on the firewall in front of port 8080 and refuse everything else. That is a much smaller opening than it looks, and it is the whole of the Retool Cloud story.
Step 5: the timeouts that decide whether a slow solve survives
Retool publishes different ceilings for different block types, and they matter here because a solve is slow by nature.
| Which limit | Value | Why it matters to a solve |
|---|---|---|
| Resource query block, asynchronous run | Up to 10 minutes | Comfortable. A single read returns in well under a second |
| Resource query block, synchronous run | Up to 2 minutes | Still fine for a read, because the waiting happens in a Wait block |
| Loop block | 10 seconds by default, 2 minutes at most | The reason a poll loop is the wrong shape here |
| Whole run, asynchronous | 30 hours, and unlimited with Wait blocks | Nothing about a solve comes close |
| Whole run, synchronous | 15 minutes up to the first webhook Response block | The trap. See the paragraph below |
| Concurrent external requests per workflow | 50 at a time | The real ceiling on a batch of solves in one run |
| Schedule trigger interval | One minute minimum | Fine, and it costs nothing to run it that often |
The synchronous number is the one to plan around. A webhook trigger that holds the connection open and answers with a Response block gives you fifteen minutes, which sounds generous until you remember that a caller sitting on an open HTTP connection for two minutes waiting on a reCAPTCHA is a bad design in its own right. Trigger the workflow asynchronously and have it post the token where it is needed, or answer the webhook immediately and do the solve behind it.
Resource query blocks also carry their own retry count and exponential backoff settings. Turn those on for submitCaptcha and leave them off for readResult, for the read-once reason above.
If you would rather write code
On a self-hosted instance where the code executor can reach the solver, the whole flow collapses into one Python block, because the SDK polls for you. Add capskip to the workflow’s requirements.txt in the Libraries tab first.
# pip install capskip - add it in the Libraries tab instead.
from capskip import CapSkip
# host is the solver machine. Keep 127.0.0.1 only when Retool
# runs on the same Windows box as CapSkip.
solver = CapSkip(host="192.168.1.40", port=8080)
result = solver.recaptcha(
sitekey="YOUR_SITEKEY",
url="https://example.com/page-with-recaptcha",
)
# Python blocks serialize their output as JSON, so return
# the token rather than the client object.
{"token": result["code"]}The SDK starts polling at 250 milliseconds and backs off to five seconds, rather than sleeping a flat interval, so this version usually returns sooner than the Wait based flow. Its ceiling for reCAPTCHA, Turnstile and GeeTest is three hundred seconds, inside the ten minute asynchronous code block timeout. Retool Cloud runs Python 3.10 by default, and the equivalent one-call versions for Node.js, PHP and C# are on the CAPTCHA solving SDK page.
Submit the token in the block that follows immediately. A reCAPTCHA token is good for about two minutes, so a workflow that solves, waits on a long Wait block and then submits will fail on a token that was valid when it was made. That failure mode is covered in the guide to reCAPTCHA token expiration.
Common errors and what they mean
| What you see | Cause | Fix |
|---|---|---|
| A code block times out reaching a LAN address | The self-hosted code executor’s default iptables rules cover 192.168.0.0/16 | Move the call into a REST resource query block |
| Connection refused on port 8080 | CapSkip is bound to loopback and Retool is somewhere else | Switch to Server mode and use the solver’s network address |
| Retool Cloud cannot reach the resource at all | The firewall does not admit Retool’s outbound addresses | Allow the published ranges for your region on port 8080 |
| readResult returns CAPCHA_NOT_READY every time | The Wait block is shorter than the solve takes | Raise the first wait, or add a second wait and read on the Else path |
| The second read of the same id comes back empty | A CapSkip result is readable once | Keep the token in the block output, never re-read the id |
| ERROR_WRONG_USER_KEY in the response | The key parameter resolved to an empty string | Check the secret name, including its exact case |
| A valid token is rejected by the target site | It expired between the solve and the submit | Submit in the next block, with no Wait between them |
| A batch of solves stalls partway through | One workflow may have 50 external requests in flight | Batch the Loop block, or split the work across runs |
FAQ
Can I use CapSkip from Retool Cloud?
Yes, with Server mode. Retool Cloud calls your resources from its own infrastructure, so the solver has to listen on an address they can reach: a public IP, ideally a static one. Retool publishes the outbound ranges it calls from, so the firewall rule is narrow rather than open to the world. Nothing about the solver changes and nothing becomes metered. The only difference is the address it listens on.
Why not just use a JavaScript block with axios?
Because of where that code runs. Retool executes code blocks in a separate service sandboxed with NsJail, and on a self-hosted deployment that service installs firewall rules that cover private ranges. Turning them off is a documented switch, but it is an instance-wide decision taken to make one workflow easier, and Retool recommends against it. A resource query block reaches the same solver with no such argument. Use code blocks for the logic and resource query blocks for the network.
Should the workflow loop until the token arrives?
No. A Loop block caps out at two minutes, well short of the three hundred seconds a reCAPTCHA solve is allowed, and it runs every iteration you gave it rather than stopping early. On top of that, a result can be read once, so repeated reads of the same id are wasted trips. A Wait block of the right length plus a single read is correct and cheap, and a Branch with a second wait and read behind it covers the slow cases.
How does this compare with n8n or Pipedream?
The two requests are identical in all three tools. What differs is the obstacle each one puts in front of them.
- For n8n the obstacle is container networking, worked through in the n8n workflow guide.
- For Pipedream it is the step runtime and where secrets live, covered in the Pipedream guide.
- For Retool it is the code executor’s own firewall, which is why the flow above never puts an HTTP call in a code block.
The short version
Point a REST resource at the solver, POST the challenge, Wait fifteen seconds, GET the result, and Branch on whether it is ready. Keep the HTTP in resource query blocks rather than code blocks, because the code executor’s default firewall rules cover private addresses and loosening them is an instance-wide decision. Switch CapSkip to Server mode whenever Retool is not on the solver’s own machine, and on Retool Cloud allow only the published outbound ranges through to port 8080. Never wait on a solve inside a synchronous webhook.
- The reCAPTCHA v2 checkbox itself is covered on the reCAPTCHA v2 solver page.
- The Python client used in the code block route is documented on the Python CAPTCHA solver page.
One thing worth weighing before you point a scheduled workflow at this every minute: CapSkip is a local captcha solver running on hardware you already own, so a workflow that fires every minute and one that fires twice a day cost exactly the same.
