This website uses cookies

Read our Privacy policy and Terms of use for more information.

Welcome to issue #05 of NowLink — a build-in-public newsletter of creating a local MCP server that connects Claude Desktop directly to ServiceNow.

In our last issue, we gave Claude the keys to ServiceNow's Flow Designer engine - and immediately realized that a single typo in a subflow input name would cause the platform to silently swallow your payload and execute an empty workflow

This week, we wipe out that structural blind spot entirely. We are breaking down how we extract hidden metadata to validate inputs before execution, expanding coverage across the entire Flow, Subflow and Action ecosystem.

Making Automation Smart 💡

In theory, triggering a ServiceNow automation from a chat window is a fantastic trick. In practice, the platform's native flexibility makes doing it blindly incredibly dangerous.

When you query data, a mistake just yields a weird answer. But when you execute workflow logic, the stakes change. If you pass an unrecognized parameter name to a ServiceNow subflow, the system doesn’t throw an error or abort the run—it happily triggers the workflow with entirely empty variables, executing a silent failure while returning a "successful" execution ID.

Before expanding NowLink's footprint into Actions and full Flows, we had to solve a foundational security gap: How do we make sure Claude actually understands the exact input contract a workflow expects before it pulls the trigger?

In this milestone walkthrough of NowLink v0.5, here is exactly what we are breaking down today:

  • The 420-Line Phantom Bug: How a massive copy-paste error hid in plain sight at module level, and the AST structural test we built to banish duplicate tools forever.

  • The Silent Failure Fix: How we reverse-engineered an undocumented JSON field label_cache to map workflow input types and names pre-flight.

  • Flows Play by Different Rules: What makes top-level flows a completely different architectural challenge, how our bridge mimics a platform record-trigger, and why scheduled workflows remain strictly out of reach.

  • The Weekly Gotcha: Why discovering custom Actions requires a developer-tier role, while Subflows run perfectly fine on standard integration accounts.

The Phantom Code: 420 Lines Bug 🪲

The single most embarrassing—and instructive—moment of this build happened inside our own server.py.

During the frantic final pushes of v0.4 development, a copy-paste error went completely unnoticed. Three primary flow tools (list_subflows, trigger_subflow, and get_flow_status) were accidentally duplicated four extra times. That is 420 lines of completely dead, unreachable code sitting at the module level.

Why didn't it break? Because Python silently allows duplicate function definitions within the same module. The final definition simply overwrites the previous ones, meaning the runtime server and our FastMCP registration framework only registered the last, correct block. Everything worked perfectly in manual testing, hiding the bloat completely.

Yeap, this is the price one has to pay for vibe-coding. 😉

  • The Fix: We stripped the duplicate blocks entirely.

  • The Prevention: We wrote tests/test_server_structure.py. It reads server.py as an Abstract Syntax Tree (AST), counts definitions programmatically, and asserts that every single @mcp.tool() is declared exactly once. If someone adds a tool or introduces a duplicate, the pipeline breaks instantly.

    def test_no_duplicate_tool_registrations(self):
        """Every @mcp.tool() function must be defined exactly once."""
        names = _get_tool_decorated_function_names()
        seen = {}
        duplicates = []
        for name in names:
            seen[name] = seen.get(name, 0) + 1
        duplicates = [n for n, count in seen.items() if count > 1]
        assert duplicates == [], (
            f"Duplicate @mcp.tool() registrations found: {duplicates}. "
            "Each tool must be defined exactly once in server.py."
        )

Testing the Code

Speaking of which - it is always a good idea to create a set of comprehensive tests to confirm you haven’t break anything by adding new functionality. In Python, the best way to do it, is via native pytest modul.

So far, we have implemented 99 test scenarios during our NowLink implementation.

Pytest example

Fixing the Silent Failure

In our previous release, NowLink had an honest platform limitation: it lacked visibility into what inputs a subflow actually expected.

If you targeted a subflow with the wrong input variable name, ServiceNow's native FlowAPI.withInputs() would quietly discard the unmatched keys, execute anyway, and run with empty values. No error logs, no validation flags—just absolute silence while your automation did exactly nothing.

To solve this, NowLink v0.5 introduces a pre-flight validation layer. Because ServiceNow does not expose a clean, public REST endpoint for input discovery under standard roles, we went hunting through the metadata schemas.

We found our answer in label_cache - a stringified JSON field sitting directly on the sys_hub_flow and sys_hub_action_type_definition tables. It maps every single variable used in an entire execution graph. By targeting keys prefixed with subflow. or isolating regex matches like action.variable_name, NowLink can extract the exact parameters expected before firing an API call, using its new tools describe_action, describe_subflow and describe_flow. Each trigger has a recommended workflow to follow:

@mcp.tool()
def trigger_action(action_name: str, inputs: dict) -> dict:
    """
    Trigger a Flow Designer action by name with input variables.

    RECOMMENDED WORKFLOW:
      1. Call list_actions to confirm the trigger_name
      2. Call describe_action to see what inputs are expected and their types
      3. Collect any missing inputs from the user
      4. Call trigger_action with the correct input names and values

The describe tool is very important because it gives NowLink a list of expected inputs to trigger the action or subflow.

Claude describe tool output

As a result, when an incorrect input is provided, NowLink is aware of it.

Triggering a subflow with incorrect field

Flows Play by Different Rules

One thing I didn't fully appreciate until I built v0.5 is how different flows and subflows actually are — not just technically, but philosophically.

Subflows and actions are designed to be called. They have declared inputs, no trigger, and exist specifically to be invoked by something else — another flow, a script, or now NowLink. When you call trigger_subflow, you're using them exactly as intended. Pass the right inputs, get an execution ID back.

Flows are designed to react. They sit idle until something happens on the platform — a record gets created, a schedule fires, a catalog item is submitted. The trigger is baked into the flow definition. When it fires, the platform injects the context automatically: the current record, the previous values, the event metadata. None of that comes from the caller. The platform owns it.

What NowLink does with trigger_flow is an honest workaround.

  • For record-triggered flows, the bridge fetches the GlideRecord server-side and passes it as current — close enough that most flows run fine. But it's not a real platform event. If a flow relies on previous values, or evaluates which fields changed, or uses the trigger condition that was originally evaluated — that stuff won't be there. The flow runs, but it runs slightly blind.

  • For scheduled flows, application-triggered flows, SLA flows — there's no workaround. NowLink doesn't pretend otherwise. If you ask Claude to trigger the SLA Notification and Escalation flow, it won't try. It will tell you what fires that flow, what table and record context it expects at runtime, and suggest that if you want equivalent logic on demand, you rebuild it as a subflow. That's not a limitation we're apologising for — it's the correct answer. Firing a scheduled flow out of band, without the scheduler context it expects, would likely produce unpredictable results anyway.

So the rule of thumb I'd suggest: if you want something triggerable on demand, build it as a subflow. That's what subflows are for. Flows are automation that the platform owns — NowLink can sometimes fire them, but you're working against the grain when you do.

The Weekly Gotcha 🪵

The Inheritance Tax on Actions

When exposing Flow Designer Actions (sys_hub_action_type_definition), our development user hit a wall of 403 Access Denied messages. Yet, Subflows (sys_hub_flow) worked effortlessly with standard rest_service permissions.

The discrepancy came down to table inheritance schemas. sys_hub_flow inherits directly from Flow Block, which carries standard access parameters. However, sys_hub_action_type_definition inherits from Application File. ServiceNow treats individual actions with the exact same security severity as scripts, access controls, and business rules.

  • The Fix: To discover and map actions, your integration profile requires the developer-tier flow_designer role.

Integration user roles

What's Next: Plugins (v0.6) 🚀

With Flow Designer completely covered (7 new tools, 99 passing tests), we are shifting our focus to extensibility.

NowLink v0.6 will introduce the NowLink Plugin System, allowing any developer to stand up custom, platform-specific AI tools in fewer than 30 lines of native Python.

What's the absolute first automated subflow or action sequence you plan to let Claude run inside your environment? Reply to this email and let me know—I read every response!

Until next week, happy coding.

— Tomáš Dolejšek, creator of NowLink

Keep Reading