Home ›  Tap Forms Pro ›  Tips & Tricks ›  Calling Tap Forms Pro Scripts from Python (and Getting Results Back) ▾ 

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:

  1. Did Tap Forms Pro open/activate? Watch your Dock when you run the script.
  2. Is the URL scheme right? Copy a script URL from the TF Pro Script Editor link button and verify it starts with tfpro://.
  3. Did the TF script actually run? Check the Script Editor output/console pane.
  4. 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 install needed)

Author


Do you like this script? Are you using it? Please consider supporting me by buying me a coffee!

Buy Me Coffee Link

Thanks!


Last modified: Feb 3, 2026 11:46:29 AM


Tap Forms Consulting

Do you need help writing scripts for your Tap Forms Pro database? I'm happy to help you. Contact me to discuss your idea or project.


Web Design Consulting

Do you need help designing your web site or getting Backlight working the way you want? Contact me to discuss your needs.


Buy me a Coffee

If you like what I share here, please consider buying me a coffee or a print. This helps keeping me motivated to continue posting. Thank you!

Buy Me A Coffee