Category: Tips & Tricks
Calling Tap Forms Pro Scripts from Python (and Getting Results Back)
Tap Forms Pro has a powerful JavaScript scripting engine, but sometimes you want to drive it from an external program — passing in parameters and getting structured data back. This post walks through a clean two-script pattern that does exactly that using the tfpro:// URL scheme and a local HTTP listener.
How It Works
Tap Forms Pro supports a custom URL scheme that lets you trigger a named Form Script from anywhere on macOS:
tfpro://script/<document_id>/<form_id>/<script_name>?key1=value1&key2=value2
Parameters passed in the query string are available inside the script via the global parameters dictionary. That covers the Python → Tap Forms direction.
For the return trip — Tap Forms → Python — we use Utils.postJsonToUrl(), a built-in TF Pro function that POSTs a JSON string to any URL. Python runs a lightweight HTTP server on localhost to receive it.
The full flow:
Python Tap Forms Pro
│ │
├─ start HTTP listener on :8765 │
├─ open tfpro:// URL ──────────────▶│
│ ├─ script runs
│ ├─ reads parameters{}
│ ├─ does its work
│◀── Utils.postJsonToUrl(result) ───┤
├─ parse JSON result │
├─ shut down listener │
└─ return result dict │
The Tap Forms Pro JavaScript Form Script
Paste this into Tap Forms Pro → Script Editor for the script you want to call.
function remote_command(){
// ─── READ PARAMETERS ─────────────────────────────────────────────────────────
var testString = parameters["test_string"] || "";
var callbackUrl = parameters["_callback"] || "";
var EXPECTED = "Hello Tap Forms Pro, you rock!";
console.log("Received test_string: " + testString);
// ─── MATCH CHECK ─────────────────────────────────────────────────────────────
var result;
if (testString === EXPECTED) {
result = {
status: "ok",
message: "String matched! Tap Forms Pro says: right back at you, Python!"
};
} else {
result = {
status: "error",
message: "String did not match. Expected: '" + EXPECTED + "', got: '" + testString + "'"
};
}
// ─── POST BACK ───────────────────────────────────────────────────────────────
if (callbackUrl) {
Utils.postJsonToUrl(JSON.stringify(result), callbackUrl);
console.log("Result posted: " + JSON.stringify(result));
} else {
console.log("[ERROR] No callback URL provided.", "#ff0000");
}
}
if (parameters){
remote_command()
} else {
console.log("[ERROR] This script only supports remote invocation using tfpro://....", "#ff0000");
}
Important: every code path in your TF script — including error branches — must call
Utils.postJsonToUrl(). If any path exits without posting back, Python will always time out on that error case.
The Python Caller
Still in the Tap Forms Pro → Script Editor, click the 🔗 link button in the editor to get the DOCUMENT_ID and FORM_ID of your script.
#! /usr/bin/env python3
import subprocess
import json
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import quote
import time
# ─── CONFIGURATION ───────────────────────────────────────────────────────────
DOCUMENT_ID = "db-xxx"
FORM_ID = "frm-xxx"
SCRIPT_NAME = "Remote Command"
LISTEN_PORT = 8765
TIMEOUT_SECS = 15
TEST_STRING = "Hello Tap Forms Pro, you rock!"
# ─────────────────────────────────────────────────────────────────────────────
_result_holder = {}
_event = threading.Event()
class ResultHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
_result_holder["data"] = json.loads(body)
except json.JSONDecodeError as e:
_result_holder["error"] = f"Tap Forms returned invalid JSON: {e}"
_result_holder["raw"] = body.decode(errors="replace")
self.send_response(200)
self.end_headers()
_event.set()
def log_message(self, *args):
pass
class TapFormsError(Exception):
pass
class TapFormsTimeoutError(TapFormsError):
pass
class TapFormsResponseError(TapFormsError):
pass
def call_tap_forms_script(params: dict) -> dict:
_event.clear()
_result_holder.clear()
callback_url = f"http://127.0.0.1:{LISTEN_PORT}/result"
params["_callback"] = callback_url
query = "&".join(f"{quote(str(k))}={quote(str(v))}" for k, v in params.items())
url = f"tfpro://script/{DOCUMENT_ID}/{FORM_ID}/{quote(SCRIPT_NAME)}?{query}"
server = HTTPServer(("127.0.0.1", LISTEN_PORT), ResultHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
time.sleep(0.3)
subprocess.run(["open", url], check=True)
if not _event.wait(timeout=TIMEOUT_SECS):
server.shutdown()
raise TapFormsTimeoutError(
f"No response from Tap Forms Pro after {TIMEOUT_SECS}s. "
f"Script '{SCRIPT_NAME}' may not have run or postJsonToUrl() failed."
)
server.shutdown()
if "error" in _result_holder:
raise TapFormsError(_result_holder["error"])
result = _result_holder.get("data", {})
if result.get("status") != "ok":
raise TapFormsResponseError(
f"Tap Forms script reported an error: "
f"{result.get('message', 'no message')} "
f"(status={result.get('status', 'missing')})"
)
return result
if __name__ == "__main__":
try:
result = call_tap_forms_script({
"test_string": TEST_STRING,
})
print(f"[SUCCESS] {result.get('message')}")
except TapFormsTimeoutError as e:
print(f"[TIMEOUT] {e}")
except TapFormsResponseError as e:
print(f"[SCRIPT ERROR] {e}")
except TapFormsError as e:
print(f"[BAD RESPONSE] {e}")
except subprocess.CalledProcessError as e:
print(f"[LAUNCH ERROR] Could not open Tap Forms Pro: {e}")
Error Handling
Three custom exceptions map to three distinct failure modes:
| Exception | Meaning |
|---|---|
TapFormsTimeoutError |
TFP never called back. Script didn't run, wrong IDs, or postJsonToUrl() failed. Check the Script Editor console. |
TapFormsResponseError |
TFP ran and responded, but the JS script set status != "ok". This is your application-level error — record not found, validation failure, etc. |
TapFormsError |
TFP responded but sent back malformed JSON. |
Debugging Tips
If you get a TapFormsTimeoutError, work through this checklist:
- Did Tap Forms Pro open/activate? Watch your Dock when you run the script.
- Is the URL scheme right? Copy a script URL from the TF Pro Script Editor link button and verify it starts with
tfpro://. - Did the TF script actually run? Check the Script Editor output/console pane.
- Test the Python listener independently while it's waiting:
curl -X POST http://127.0.0.1:8765/result \
-H "Content-Type: application/json" \
-d '{"status":"ok","data":{"test":true}}'
If this succeeds, the Python side is fine and the problem is inside TF.
Requirements
- Python 3.6+ (uses only the standard library — no
pip installneeded)
Author
- Daniel Leu, info@danielleu.com
Do you like this script? Are you using it? Please consider supporting me by buying me a coffee!
Thanks!
Last modified: Feb 3, 2026 11:46:29 AM
