# Spatial9 for Enterprise AI: Databricks, Snowflake, Vertex AI and Bedrock Pipelines for Spatial Audio

> Run governed spatial audio renders from Databricks, Snowflake, Google Vertex AI and Amazon Bedrock, including a free Databricks Solutions Accelerator that uses the Spatial9 MCP server, A2A agent and REST API.

Web page: https://spatial9.ai/blog/spatial9-enterprise-agents-databricks-snowflake-vertex-bedrock
Author: Spatial9 Team
Published: 2026-11-02. Updated: 2026-11-02.

## Key takeaways
- Spatial9 works inside Databricks, Snowflake, Google Vertex AI and Amazon Bedrock through one connection: the Spatial9 MCP server at spatial9.ai/mcp with a service key kept in the platform secret store.
- Every recipe uses the same queue pattern: one table of tracks, one job every 15 minutes, at most 3 renders at once, an idempotency_key per track version, and every status written back for audit.
- Agents are optional. Databricks AI Gateway, Snowflake Cortex custom tools, Google ADK McpToolset and Strands MCPClient on Bedrock let an agent call Spatial9 tools, while scheduled jobs need no agent at all.
- All the code is free to download from /agents/skills, including a shared Python MCP client, SQL setup for Databricks and Snowflake, a Cloud Run job and a Step Functions state machine. Microsoft Fabric is not supported.
- The Spatial9 Solutions Accelerator for Databricks is a free Asset Bundle that routes each track through the MCP server, the A2A agent or the Spatializer REST API, with Auto Loader intake, safe retries, an audit table and gold views for dashboards.

Quick answer: Enterprises can run Spatial9 immersive audio renders from the data and AI platforms they already use. Store a Spatial9 service key in the platform's secret store, allow outbound traffic to spatial9.ai, and call the Spatial9 MCP server at https://spatial9.ai/mcp from a Databricks Lakeflow Job, a Snowflake Python procedure, a Google Cloud Run job or Vertex AI agent, or an AWS Lambda or Amazon Bedrock agent. A queue table and a job every 15 minutes keep up to 3 renders running, and every result is written back for audit.

Our Agent Skills guide showed creators and small teams how to drive Spatial9 from n8n, Zapier, Make and coding agents. This is the enterprise edition. It is written for data engineers, platform teams and ML engineers at labels, publishers, streaming services, broadcasters and game studios who need catalog-scale renders that pass a security review.

Download every enterprise recipe: browse the enterprise examples online, read the spatial9-enterprise-catalog skill, or get everything in one file with spatial9-agent-skills.zip.

[Image: Your platform stays in charge of data, keys and schedules. Spatial9 only does the audio.]


## At a glance

| Question | Answer |
| Which platforms are covered? | Databricks, Snowflake, Google Cloud with Vertex AI, and AWS with Amazon Bedrock |
| What do they connect to? | The Spatial9 MCP server at https://spatial9.ai/mcp with Authorization: Bearer s9_... |
| Do I need an AI agent? | No. Scheduled jobs work without one. Agents are an option on every platform |
| Where does the key live? | Databricks secret scopes, Snowflake secrets, Google Secret Manager or AWS Secrets Manager |
| Limits | 3 renders at once and 20 per day per account. Failed renders are refunded |
| Outputs | Eclipsa Audio (IAMF), a binaural headphone WAV, a YouTube-ready MP4 and stems |
| Not supported | Microsoft Fabric |


## Why enterprises render spatial audio from their data platform

Catalogs already live in data platforms. Track metadata, rights, release dates and links to masters sit in Delta tables, Snowflake schemas, BigQuery datasets or S3 buckets. Moving those files by hand to a separate audio tool breaks the audit trail and does not scale beyond a few hundred songs.

Running renders where the catalog lives gives you four things a manual process cannot:

- One source of truth. The same table that holds rights and release dates holds the render status, job id and file paths.
- Governance you already trust. Keys sit in your vault, outbound traffic is allowlisted, and access follows the grants your security team already approved.
- Repeatability. A render is a row. Re-running a job never starts a second render because every request carries an idempotency_key.
- Scale without babysitting. A small scheduled job keeps 3 renders busy around the clock and leaves the rest of the queue for later.

If you are new to the format, Eclipsa Audio versus Dolby Atmos and our IAMF deep dive explain what lands in your storage at the end.

[Image: A back catalog is a table of rows. Each row can become an immersive release.]


## The building blocks every platform shares

Every recipe in this guide uses the same five pieces. Once you understand them on one platform, the others are a change of names.

| Piece | What it does | Databricks | Snowflake | Google Cloud | AWS |
| Compute | Runs the code | Lakeflow Job | Python procedure | Cloud Run job | Lambda |
| Secret | Holds the s9_ key | Secret scope | Secret object | Secret Manager | Secrets Manager |
| Schedule | Wakes the job | Job schedule | Task | Cloud Scheduler | Step Functions or EventBridge |
| Storage | Keeps finished files | Unity Catalog Volumes | Stage | Cloud Storage | S3 |
| Agent (optional) | Lets people ask in plain words | AI Gateway MCP Services | Cortex Agent custom tool | ADK on Agent Engine | Strands on Bedrock or AgentCore |

[Image: The same pipeline in four ecosystems. Pick the column your team already runs.]


### The Spatial9 tools you will call

You only need four tools for a catalog pipeline:

- create_upload_link returns a direct_upload_url and an upload_id for files that are not on a public link. Links last 2 hours and accept up to 2 GB.
- create_spatial_audio starts a render from upload_id or source_url, with media_type, title, style, include_binaural and an idempotency_key.
- get_job_status returns status, stage and progress. Songs usually take 10 to 20 minutes.
- get_downloads lists the finished files, including binaural, iamf and youtube_video.

Teams that start from multitrack sessions can use create_from_stems, teams with Dolby Atmos masters can use convert_dolby_to_eclipsa (see Atmos to Eclipsa), and split_stems separates a stereo master first.


## Step 1: the MCP handshake in plain Python

Most enterprise runtimes give you Python and the requests library, but not always an MCP SDK. The Spatial9 server uses the standard MCP Streamable HTTP transport, which is three messages: initialize, notifications/initialized, then any number of tools/call requests. If the server returns an Mcp-Session-Id header, send it back on every later request.

[Image: Three messages, then any tool. Replies can be plain JSON or a short event stream.]

This is the shared client used by the Databricks, Google Cloud and AWS recipes. The full file is spatial9_mcp.py.

```python
import itertools, json, requests

PROTOCOL_VERSION = "2025-06-18"


class Spatial9MCP:
    def __init__(self, api_key: str, url: str = "https://spatial9.ai/mcp"):
        self.url = url
        self.session_id = None
        self._ids = itertools.count(1)
        self.headers = {
            "Authorization": f"Bearer {api_key}",          # read from your secret store, never hard-code
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream",  # the server may answer with either
        }
        # 1. initialize, 2. tell the server we are ready
        self._post({"jsonrpc": "2.0", "id": next(self._ids), "method": "initialize",
                    "params": {"protocolVersion": PROTOCOL_VERSION, "capabilities": {},
                               "clientInfo": {"name": "spatial9-enterprise", "version": "1.0.0"}}})
        self._post({"jsonrpc": "2.0", "method": "notifications/initialized"})

    def _post(self, payload):
        headers = dict(self.headers)
        if self.session_id:                                  # echo the session on later calls
            headers["Mcp-Session-Id"] = self.session_id
            headers["MCP-Protocol-Version"] = PROTOCOL_VERSION
        r = requests.post(self.url, json=payload, headers=headers, timeout=120)
        r.raise_for_status()
        self.session_id = r.headers.get("Mcp-Session-Id", self.session_id)
        if "id" not in payload or r.status_code == 202:      # notifications have no reply body
            return None
        if "text/event-stream" in r.headers.get("Content-Type", ""):
            data = [l[5:].strip() for l in r.text.splitlines() if l.startswith("data:") and l[5:].strip()]
            return json.loads(data[-1])                      # the last message is the answer
        return r.json()

    def call(self, name, arguments):
        """3. tools/call. Returns structuredContent or raises with a plain-language reason."""
        reply = self._post({"jsonrpc": "2.0", "id": next(self._ids), "method": "tools/call",
                            "params": {"name": name, "arguments": arguments}}) or {}
        if "error" in reply:
            raise RuntimeError(reply["error"]["message"])
        result = reply.get("result", {})
        if result.get("isError"):
            raise RuntimeError(result["content"][0]["text"])
        return result.get("structuredContent", {})
```

Tip: If your platform already ships an MCP client (Google ADK, Strands Agents, the Databricks MCP client), use it for agents. Keep this small client for scheduled jobs, where fewer packages mean fewer surprises.


## Step 2: the queue pattern

Renders take minutes, not milliseconds, so the safest design is not one long-running process. It is a table and a short job that wakes up every 15 minutes.

[Image: One table, one scheduled job. No servers to keep alive and nothing lost if a run is skipped.]

Each run does five things in order:

- Check running rows. Call get_job_status for every row marked running.
- Save finished files. For completed jobs, call get_downloads and copy the files to governed storage.
- Count free slots. Three renders can run at once, so free slots are 3 minus the rows still running.
- Start pending rows. Call create_spatial_audio with idempotency_key = "<platform>-<track_id>-v<version>".
- Write status back. Store job_id, stage, file paths, any error and the time.

If the daily limit of 20 is reached, the tool explains it in plain words. The job stops starting new rows and tries again on the next run. A failed render refunds its project automatically, so the row is marked failed with the reason and a person decides whether to retry.


## Databricks: Lakeflow Jobs, Unity Catalog and Volumes

Databricks is the natural home for catalogs that already sit in Delta tables. The recipe uses a secret scope for the key, a Unity Catalog HTTP connection so access is governed by grants, a queue table, and two Volumes for masters and finished files.


### One-time setup

```sql
-- Store the key first, from the Databricks CLI:
--   databricks secrets create-scope spatial9
--   databricks secrets put-secret spatial9 api_key

-- A governed HTTP connection. People and agents can use it without ever seeing the key.
CREATE CONNECTION IF NOT EXISTS spatial9_mcp TYPE HTTP
OPTIONS (
  host 'https://spatial9.ai',
  port '443',
  base_path '/mcp',
  bearer_token secret('spatial9', 'api_key')
);
GRANT USE CONNECTION ON CONNECTION spatial9_mcp TO `catalog-engineers`;

-- One row per track and version. The status column is the source of truth.
CREATE TABLE IF NOT EXISTS media.spatial.renders_queue (
  track_id STRING NOT NULL, version INT NOT NULL DEFAULT 1, title STRING NOT NULL,
  source_path STRING, style STRING DEFAULT 'live_concert', status STRING DEFAULT 'pending',
  job_id STRING, stage STRING, binaural_path STRING, iamf_path STRING, error STRING, updated_at TIMESTAMP
) TBLPROPERTIES (delta.feature.allowColumnDefaults = 'supported');

CREATE VOLUME IF NOT EXISTS media.spatial.masters;    -- input WAV files
CREATE VOLUME IF NOT EXISTS media.spatial.immersive;  -- finished files land here
```


### The job

Create a Lakeflow Job with one Python task on a 15-minute schedule. This is the heart of spatial9_catalog_job.py:

```python
from spatial9_mcp import Spatial9MCP, Spatial9Error

QUEUE = "media.spatial.renders_queue"
s9 = Spatial9MCP(api_key=dbutils.secrets.get("spatial9", "api_key"))  # never printed, redacted in logs

# Start new renders only while fewer than 3 are running.
free = 3 - spark.table(QUEUE).where("status = 'running'").count()
for row in spark.table(QUEUE).where("status = 'pending'").limit(max(free, 0)).collect():
    try:
        job = s9.call("create_spatial_audio", {
            "upload_id": upload(row.source_path),        # Volume file -> upload link -> upload_id
            "media_type": "music",
            "title": row.title,
            "style": row.style or "live_concert",       # live_concert, club, cinema or intimate_studio
            "include_binaural": True,                    # always ship a headphone version
            "idempotency_key": f"databricks-{row.track_id}-v{row.version}",
        })
        update(row.track_id, row.version, status="running", job_id=job["job_id"], stage="queued")
    except Spatial9Error as e:
        update(row.track_id, row.version, error=str(e))   # plain-language reason from Spatial9
        if "limit" in str(e).lower():
            break                                        # daily limit: try again next run
```

The update helper uses parameterised spark.sql(..., args=...), so titles with quotes never break the statement.


### Letting Databricks agents call Spatial9

Databricks lets you register an outside MCP server through Unity AI Gateway (MCP Services), backed by a Unity Catalog connection like spatial9_mcp. Agents built with Mosaic AI Agent Framework then call Spatial9 tools under their own identity, and every call is governed by USE CONNECTION grants. Menus and API names are evolving quickly, so check the current Databricks documentation for the exact steps in your workspace.


## The Databricks Solutions Accelerator: MCP, A2A and REST API in one bundle

The recipe above renders a catalog. The Spatial9 Solutions Accelerator for Databricks is the complete, deployable version: one Databricks Asset Bundle, six notebooks and a scheduled job that takes a music catalog from a Unity Catalog Volume to finished immersive files, with an audit trail and dashboards. It uses all three ways to reach Spatial9, so each track goes the way that suits it best.

Get it: browse the solution-accelerator folder online or download everything in spatial9-agent-skills.zip. It is MIT licensed.

[Image: New masters land in a Volume, Auto Loader queues them once, and each row is routed to MCP, A2A or the REST API. Every result lands in the same Volume, audit table and gold views.]


### Who it is for

Labels, publishers, distributors, streaming services and broadcasters that keep their catalog and metadata in Databricks and want immersive versions of thousands of tracks without building a pipeline from scratch. A data engineer can deploy it in an afternoon, and the security team gets secret scopes, grants and an append-only audit table from day one.


### Three routes, one queue

Every track is one row in a Delta table called render_queue. The route column decides how it reaches Spatial9:

| Route | Interface | Best for | You get back |
| mcp | Spatial9 MCP server at spatial9.ai/mcp | Stereo masters that need the full Spatial9 render | Eclipsa Audio (IAMF), a YouTube-ready MP4 and a binaural headphone WAV |
| a2a | Spatial9 A2A agent at spatial9.ai/a2a | Handing a whole task to Spatial9 and following it | Stems as WAV, Dolby or ADM converted to Eclipsa Audio, or a free 30-second chorus preview |
| api | Spatializer REST API | Labels that already have stems and need delivery masters | A Dolby Atmos master and IAMF from the same job |

[Image: MCP and A2A share the same s9_ key and plan limits. The REST API is for stem-based delivery masters.]


### What is in the bundle

| File | What it does |
| databricks.yml | Asset Bundle: one job with five tasks every 15 minutes, paused until you unpause it |
| 00_config.py | Widgets for catalog, schema and secret scope, plus helpers shared by every notebook |
| 01_setup.py | Schema, four Volumes (masters, stems, immersive, checkpoints), the queue and audit tables, and an optional governed HTTP connection |
| 02_ingest_masters.py | Auto Loader picks up new WAV, FLAC or AIFF files and queues each one exactly once |
| 03_render_mcp.py | Full immersive renders with create_spatial_audio, get_job_status and get_downloads |
| 04_delegate_a2a.py | Stems, Dolby or ADM conversion and chorus previews through the A2A agent |
| 05_atmos_rest_api.py | Stems to Dolby Atmos and IAMF with resumable tus uploads |
| 06_gold_views.sql | Catalog coverage, daily throughput and failure reasons for AI/BI dashboards or Genie |

The job runs ingest first, then MCP and the REST API in parallel, then A2A, then refreshes the gold views. max_concurrent_runs: 1 means two runs never compete for the same render slots.


### The rule that keeps it safe: free slots before anything starts

Spatial9 allows 3 renders at once and 20 per day on a key, shared by MCP and A2A. Instead of starting work and handling errors, every notebook asks how many slots are free first:

```python
def free_slots() -> int:
    """How many new renders may start now, given the 3-at-once and 20-per-day limits."""
    running = spark.table(QUEUE).where(
        "status = 'running' AND route IN ('mcp', 'a2a') AND skill <> 'preview_chorus'"  # previews are free
    ).count()
    today = spark.sql(
        f"SELECT count(*) n FROM {EVENTS} WHERE event = 'started' AND at > current_timestamp() - INTERVAL 24 HOURS"
    ).first().n
    return max(0, min(MAX_RUNNING - running, DAILY_LIMIT - today))
```

Combined with an idempotency key per track version (databricks-<track>-v<version> for MCP and the messageId for A2A), a retried or overlapping run can never start, or charge, a second render.


### Delegating to the A2A agent

With A2A, Databricks does not call tools one by one. It hands Spatial9 a task and follows its state from submitted to working to completed:

```python
from spatial9_a2a import Spatial9A2A

a2a = Spatial9A2A(api_key=secret("api_key"))          # same s9_ key as MCP, read from the secret scope

task = a2a.send(
    message_id=f"databricks-split_stems-{row.track_id}-v{row.version}",  # idempotent: same id, same task
    skill="split_stems",                                  # or convert_dolby, preview_chorus, spatial_audio
    data={"upload_id": upload(row.source_path), "title": row.title},
)

# Next run: follow it, then save every artifact into the immersive Volume.
task = a2a.get(task["id"])
if a2a.state(task) == "completed":
    for f in a2a.files(task):                             # links come from Spatial9, never built by hand
        save(f["url"], f"{IMMERSIVE}/{row.track_id}-v{row.version}-{f['name']}")
```

Prefer push to polling? a2a.set_webhook(task_id, url, token) makes Spatial9 post the task to an https endpoint, such as a Databricks App, each time its state changes. Check the X-A2A-Notification-Token header before trusting the body.


### Stems to Dolby Atmos with the REST API

For the api route, each row points at a folder of stems and an instruments map of instrument IDs to file names (the notebook lists the IDs from GET /instruments). The notebook creates the music with POST /musics?formats=atmos,iamf, uploads every stem with the resumable tus flow (POST to start, HEAD for the offset, PATCH in 32 MB chunks), and downloads the Dolby Atmos and IAMF masters with GET /musics/{id}/download once they are ready. A dropped connection on a 2 GB stem resumes where it stopped instead of starting over.


### Deploy in five steps

- Create a key at spatial9.ai/connect, then store it: databricks secrets create-scope spatial9 and databricks secrets put-secret spatial9 api_key. Add spatializer_api_key only if you use the REST API route.
- Download the repo and open a terminal in examples/enterprise/databricks/solution-accelerator.
- Run databricks bundle deploy -t dev --var warehouse_id=<your SQL warehouse id>.
- Run 01_setup.py once, then drop a few WAV files into the masters Volume.
- Run databricks bundle run spatial9_accelerator -t dev. When the first files look and sound right, unpause the schedule.


### What you can measure

The gold views answer the questions a catalog owner asks every week: how much of the catalog is immersive already (gold_catalog_coverage), how many renders ran per day against the limit and how long they took (gold_daily_throughput), and which plain-language reasons Spatial9 gave for failures (gold_failure_reasons). Point a Genie space at them and people can ask "How many jazz tracks are still waiting?" in plain English.


### Letting Mosaic AI agents use it too

01_setup.py creates the spatial9_mcp HTTP connection. Register it as an external MCP server in Unity AI Gateway (MCP Services) and Mosaic AI agents can call Spatial9 tools under their own identity, governed by USE CONNECTION grants, while the scheduled job keeps the bulk work flowing. Databricks menus change often, so check the current documentation for the exact steps.


## Snowflake: external access, a Python procedure and a Task

Snowflake blocks outbound traffic by default, which is exactly what security teams want. You open one door to spatial9.ai, attach a secret, and run the logic as a Python stored procedure on a schedule.

```sql
-- Outbound traffic only to spatial9.ai.
CREATE OR REPLACE NETWORK RULE spatial9_egress
  MODE = EGRESS TYPE = HOST_PORT VALUE_LIST = ('spatial9.ai');

-- The key is a secret object, readable only by the procedure owner.
CREATE OR REPLACE SECRET spatial9_key TYPE = GENERIC_STRING SECRET_STRING = 's9_replace_me';

-- Tie the rule and the secret together.
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION spatial9_access
  ALLOWED_NETWORK_RULES = (spatial9_egress)
  ALLOWED_AUTHENTICATION_SECRETS = (spatial9_key)
  ENABLED = TRUE;
```

The procedure declares both the integration and the secret, then reads the key at run time:

```sql
CREATE OR REPLACE PROCEDURE SPATIAL9_RUN(MAX_RUNNING NUMBER DEFAULT 3)
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = '3.11'
PACKAGES = ('snowflake-snowpark-python', 'requests')
HANDLER = 'run'
EXTERNAL_ACCESS_INTEGRATIONS = (spatial9_access)   -- the only door out
SECRETS = ('key' = spatial9_key)                    -- alias used in the code below
EXECUTE AS OWNER
AS
$$
import _snowflake

def run(session, max_running):
    s9 = Client(_snowflake.get_generic_secret_string("key"))   # key never leaves Snowflake
    # 1. finish running rows, 2. start pending rows up to max_running, 3. write status back
    ...
$$;

-- Every 15 minutes, on a small warehouse.
CREATE OR REPLACE TASK SPATIAL9_EVERY_15_MIN
  WAREHOUSE = TRANSFORM_XS
  SCHEDULE = 'USING CRON */15 * * * * UTC'
AS CALL SPATIAL9_RUN(3);
ALTER TASK SPATIAL9_EVERY_15_MIN RESUME;
```

The complete procedure, with the MCP handshake inlined because procedures cannot import local files, is in spatial9_procedure.sql. Masters are passed as presigned SOURCE_URL links so no file needs to leave a stage by hand.

Good to know: Snowflake's own MCP server feature lets outside agents read Snowflake data. It does not make Snowflake call outside MCP servers. To let a Cortex Agent start renders, register SPATIAL9_RUN (or one small procedure per tool) as a custom tool. The agent then inherits the same network rule, secret and grants.


## Google Cloud and Vertex AI: ADK agents and Cloud Run jobs

Google Cloud gives you two clean options. Use a Cloud Run job plus Cloud Scheduler for a nightly catalog run with no agent, or an Agent Development Kit (ADK) agent on Vertex AI Agent Engine when people want to ask for renders in plain words.


### An ADK agent with Spatial9 tools

```python
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams

spatial9 = McpToolset(
    connection_params=StreamableHTTPConnectionParams(
        url="https://spatial9.ai/mcp",
        headers={"Authorization": f"Bearer {spatial9_key()}"},  # from Secret Manager
        timeout=60,
        sse_read_timeout=600,
    ),
    # Least privilege: this internal bot cannot publish anything publicly.
    tool_filter=["create_upload_link", "create_spatial_audio", "create_from_stems",
                 "get_job_status", "get_downloads", "preview_chorus", "split_stems"],
)

root_agent = LlmAgent(
    name="spatial9_catalog_agent",
    model="gemini-2.5-flash",
    instruction=("Only render music the team owns. Always send an idempotency_key built from the "
                 "track id. At most 3 renders at once. Only share links returned by get_downloads."),
    tools=[spatial9],
)
```

Deploy it with deploy_agent_engine.py. The Agent Engine SDK changes between releases, so pin the versions you test with.


### A nightly Cloud Run job

The batch job reads pending rows from BigQuery, runs 3 workers, streams finished files into Cloud Storage and writes results back. The key arrives as an environment variable mounted from Secret Manager, never baked into the image:

```bash
gcloud run jobs deploy spatial9-catalog \
  --source . --region us-central1 \
  --service-account spatial9-runner@your-project.iam.gserviceaccount.com \
  --set-secrets SPATIAL9_KEY=spatial9-api-key:latest \
  --task-timeout 90m --max-retries 1

# Every night at 02:00 UTC
gcloud scheduler jobs create http spatial9-nightly --location us-central1 \
  --schedule "0 2 * * *" \
  --uri "https://run.googleapis.com/v2/projects/your-project/locations/us-central1/jobs/spatial9-catalog:run" \
  --http-method POST --oauth-service-account-email spatial9-runner@your-project.iam.gserviceaccount.com
```

ADK can also talk to remote agents over A2A. If you prefer agent-to-agent tasks with webhooks over tool calls, Spatial9 has an A2A endpoint at https://spatial9.ai/a2a, covered in our A2A integrations guide.

[Image: Pipelines do the repetitive work. People still do the listening and the approving.]


## AWS and Amazon Bedrock: Strands agents, Lambda and Step Functions

On AWS, the most robust catalog pattern is a Step Functions state machine. A Map state runs 3 tracks at a time, a Wait state pauses 45 seconds between status checks, and one Lambda does the Spatial9 calls. You pay for seconds of work, not for minutes of waiting.

```json
{
  "Type": "Map",
  "ItemsPath": "$.tracks",
  "MaxConcurrency": 3,
  "ItemProcessor": {
    "StartAt": "Start",
    "States": {
      "Start":  { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Next": "Wait" },
      "Wait":   { "Type": "Wait", "Seconds": 45, "Next": "Status" },
      "Status": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Next": "Done?" },
      "Done?":  { "Type": "Choice", "Default": "Wait",
                  "Choices": [{ "Variable": "$.status", "StringEquals": "completed", "Next": "Save" }] },
      "Save":   { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "End": true }
    }
  }
}
```

The shortened version above leaves out payloads and the failed branch. The full state_machine.json and spatial9_lambda.py include them, with the key read from Secrets Manager and files streamed into S3.


### A Bedrock agent with Strands

```python
from mcp.client.streamable_http import streamablehttp_client
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient

spatial9 = MCPClient(lambda: streamablehttp_client(
    "https://spatial9.ai/mcp",
    headers={"Authorization": f"Bearer {spatial9_key()}"},   # from AWS Secrets Manager
))

with spatial9:                                                 # keeps the MCP session open
    tools = [t for t in spatial9.list_tools_sync() if t.tool_name != "publish_to_youtube"]
    agent = Agent(model=BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0"),
                  system_prompt="Only render music the team owns. At most 3 renders at once.",
                  tools=tools)
    agent("Make an immersive live concert version of track 0042 and tell me the job id.")
```

Amazon Bedrock AgentCore adds managed pieces around this: Runtime can host the agent, Gateway can front Spatial9 as an MCP target so many agents share one governed connection, and Identity can hold the key. Classic Bedrock Agents can call the same Lambda as an action group. Service names and console steps change often, so confirm details in the current AWS documentation.


## Governance: six controls your security team will ask about

[Image: Bring this checklist to your security review. Every recipe implements all six.]

- Secrets, not strings. The s9_ key lives in the platform vault and is rotated at spatial9.ai/connect. It never appears in notebooks, prompts or logs.
- Egress allowlist. Outbound traffic goes only to spatial9.ai, plus the file host returned by get_downloads if you copy files.
- Idempotency. One key per track and version. A retried job returns the same render instead of starting a second one.
- Respect the limits. 3 renders at once and 20 per day. The queue waits instead of failing.
- Audit trail. job_id, status, stage, file paths, errors and timestamps are written back next to the track's rights data.
- Rights first, least privilege. Only render music you own or license. Give each agent only the tools it needs, and keep public publishing behind a human approval.


## Other enterprise tools

The shared Python client works in any runtime that can make HTTPS calls, so the same pattern fits Prefect and Dagster flows, Apache Airflow (see the Airflow DAG), and GitHub Actions. Enterprise agent platforms such as Salesforce Agentforce, ServiceNow and IBM watsonx Orchestrate are adding support for remote MCP servers. Where they support one, point them at https://spatial9.ai/mcp with a service key. Check each vendor's current documentation, since these features are new and often in preview.

Microsoft Fabric is not covered by these recipes and is not a supported platform.


## Common mistakes and how to avoid them

- One long process that waits for every render. It times out and loses state. Use the queue pattern instead.
- A new idempotency key on every retry. That starts a second render. Build the key from the track id and version only.
- Starting 50 renders at once. Only 3 run at a time. Count running rows first.
- Building download links by hand. Links come only from get_downloads.
- Logging full download URLs or the key. Log the job id and file name instead.
- Skipping the listening check. Have a person or a QC step listen to the binaural WAV before release. Our mixing best practices explain what to listen for.


## Frequently asked questions


### Can Spatial9 run inside Databricks?

Yes. A Lakeflow Job calls the Spatial9 MCP server with a key from a Databricks secret scope, reads masters from Unity Catalog Volumes and writes immersive files and statuses back to a Delta table. Agents can use Spatial9 through Unity AI Gateway MCP Services.


### How do I call Spatial9 from Snowflake?

Create a network rule for spatial9.ai, a secret and an external access integration, then run a Python stored procedure that calls the MCP server. Schedule it with a Task, or register it as a custom tool for a Cortex Agent.


### Does Spatial9 work with Google Vertex AI?

Yes. Google's Agent Development Kit connects to the Spatial9 MCP server with McpToolset, and the agent can be deployed to Vertex AI Agent Engine. For scheduled renders without an agent, use a Cloud Run job with Cloud Scheduler.


### Can Amazon Bedrock agents use Spatial9?

Yes. Strands Agents connects to the Spatial9 MCP server with MCPClient using a Bedrock model. For catalog jobs, a Step Functions state machine with a Lambda handles 3 renders at a time and saves files to S3.


### How many renders can an enterprise pipeline run?

Up to 3 renders at once and 20 per day per account. The queue pattern keeps 3 running and leaves the rest pending for the next run. Failed renders are refunded automatically.


### What is the Spatial9 Solutions Accelerator for Databricks?

A free, deployable Databricks Asset Bundle with six notebooks that take a music catalog from a Unity Catalog Volume to finished immersive files. It routes each track through the Spatial9 MCP server, the A2A agent or the Spatializer REST API, respects 3 renders at once and 20 per day, retries safely with idempotency keys, and writes an audit table and gold views for dashboards. Download it from /agents/skills/examples/enterprise/databricks/solution-accelerator.


### When should I use MCP, A2A or the REST API from Databricks?

Use MCP for full immersive renders of stereo masters with tool-by-tool control. Use A2A to hand a whole task to the Spatial9 agent, such as stem splitting, Dolby or ADM conversion or a free chorus preview, and follow it by polling or webhook. Use the REST API when you already have stems and need Dolby Atmos and IAMF delivery masters.


### Is Microsoft Fabric supported?

No. These recipes cover Databricks, Snowflake, Google Cloud and Vertex AI, and AWS with Amazon Bedrock.


### Where can I download the code?

All recipes are free under the MIT license at spatial9.ai/agents/skills, in the examples/enterprise folder, or as one zip file from the same page.


## Sources

- Model Context Protocol specification, version 2025-06-18, Streamable HTTP transport, modelcontextprotocol.io.
- Databricks documentation: Unity Catalog HTTP connections, secret scopes, Volumes, Auto Loader, Lakeflow Jobs, Databricks Asset Bundles and external MCP servers.
- Agent2Agent (A2A) protocol specification, version 0.3.0, a2a-protocol.org.
- tus resumable upload protocol, version 1.0.0, tus.io.
- Snowflake documentation: network rules, secrets, external access integrations, Python stored procedures, Tasks and Cortex Agents.
- Google Cloud documentation: Agent Development Kit MCP tools, Vertex AI Agent Engine, Cloud Run jobs, Cloud Scheduler and Secret Manager.
- AWS documentation: Strands Agents MCP tools, Amazon Bedrock AgentCore, AWS Step Functions Map and Wait states, Lambda and Secrets Manager.
- Alliance for Open Media. Immersive Audio Model and Formats (IAMF) specification.

