How it Works

Connection CLI & cron

The verb surface inside the workspace — list, query, describe, browse, download, upload, schedule.

The MCP tools get your AI into the workspace. Once a connection exists, almost all real work happens inside the workspace pod: the connection CLI for data work, and the standard Linux cron daemon for scheduled jobs. Your AI runs both by passing commands to workspace_shell.

The split is deliberate. MCP tools are a narrow, slow-changing surface. The verb-level work that evolves with each connector lives in a CLI that ships inside the workspace, where every flag is discoverable with --help and every change is visible to the user as a CLI version, not a silently-changing MCP tool.

Calling shape

workspace_shell("connection <verb> [args] --json")

Always pass --json. The JSON envelope is what your AI parses; without it you get human-formatted text that's harder to work with.

The connection name is a positional argument (connection query <name> ...), not a flag.

JSON envelope

Every --json response carries at least:

{ "success": true | false, "operation": "<verb>", ... }

On failure: error, usually message, and often next_actions and (for unknown types) suggested_types. On success: verb-specific fields documented below.

Capabilities are authoritative

connection list --json returns each connection's capabilities array. That list is the truth — never call browse, download, or upload on a connection unless that verb appears in its capabilities. Capabilities depend on the connection's type and configuration; calling a non-advertised verb returns an error instead of doing the work.

The connections/<name>/README.md file mirrors the same capability list. Both come from the same source.

connection verbs

list

workspace_shell("connection list --json")

Discover connections visible in the workspace. Run this first whenever you don't already know what's available, and re-run it after adding a connection or installing a demo to confirm it appeared. In sessions that expose it, the connections_list MCP tool answers the same question.

{
  "success": true,
  "operation": "list",
  "count": 3,
  "connections": [
    { "name": "snowflake-demo", "type": "snowflake",
      "capabilities": ["query", "test", "describe"],
      "workspace_path": "/workspace/connections/snowflake-demo" }
  ]
}

add

workspace_shell("connection add --type <type> [--intent <text>] --json")

Generate a browser setup URL for a credentialed connection. Open the URL to enter credentials in the web app. You can also start directly from Connections.

--type accepts canonical type codes (pg, mysql, snowflake, bigquery, s3, salesforce, google_drive, local_file, …). Non-canonical hints can be resolved by passing --intent "<free text>".

test

workspace_shell("connection test <name> --json")

Verify stored credentials. Run after a new connection is added, after the user changes credentials, or when a previously-working query starts failing with auth-shaped errors. There's no need to test before every query — credentials don't expire on a per-call basis.

describe

workspace_shell("connection describe <name> [--database <db>] [--table <t>] --json")

Write metadata snapshots into connections/<name>/metadata/ and return the structure at the requested level. Drill-down is progressive:

  • no --database → list databases
  • --database <db> → list tables in that database
  • --database <db> --table <t> → list columns in that table

The snapshot files (not the inline response) are the durable in-workspace reference for query authoring. Re-run describe only when the snapshot is missing or stale, or when a query fails because the structure no longer matches.

query

workspace_shell("connection query <name> --file <query-file> [--params-json <json>] [--include-results] --json")

Execute a saved query file against a connection. The full result is materialized into DuckDB. By default the response is just the relation handle — relation_name, row_count, and column_count — so a large result never floods the conversation:

{
  "success": true,
  "operation": "query",
  "connection": "snowflake-demo",
  "run_id": "...",
  "relation_name": "snowflake_demo_top_customers",
  "row_count": 17,
  "column_count": 4,
  "next_actions": ["Results not included. Re-run with --include-results ..."]
}

Pass --include-results to also return the rows in data, as a JSON-encoded string of records. Inline rows are capped at 1 MiB; when a result is larger, the response includes data_truncated with how many rows came back. Bound large results with a SQL LIMIT, or aggregate in DuckDB.

Why file-based, not inline. --file requires an existing workspace file with a .sql, .json, .yaml, or .yml extension — use the format the connection's SYNTAX.md documents. This is deliberate: query files in connections/<name>/queries/ become a durable record of what was run, get committed alongside the rest of the workspace, can be reused by scheduled jobs, and let you see and edit the SQL without round-tripping through the AI. Inline SQL would be invisible after the call returned.

For provider-specific operations that aren't expressible as a query file (some storage browsers, custom RPC calls), use --op-json <json> instead of --file.

Path semantics. Paths resolve from /workspace, not from the current directory. Use connections/<name>/queries/<file>, not queries/<file>. Absolute paths under /workspace also work.

Follow-up analysis through DUCKDB. Each query materializes a DuckDB relation. Reference relation_name from a query against DUCKDB to filter, join, or aggregate without re-hitting the upstream system:

workspace_shell("""cat > connections/DUCKDB/queries/<file>.sql <<'SQL'
SELECT * FROM <relation_name> WHERE ...
SQL""")
workspace_shell("connection query DUCKDB --file connections/DUCKDB/queries/<file>.sql --include-results --json")

browse (capability-gated)

workspace_shell("connection browse <name> [--remote-path <provider-path>] [--detailed] --json")

List files at a provider-side path on storage connections (S3, Azure Blob, Google Drive, OneDrive, SFTP, …). Without --remote-path, may list the root, or — for connections that require explicit bucket selection — return an available_buckets array:

{
  "success": true,
  "operation": "browse",
  "execution_mode": "bucket_selection_required",
  "available_buckets": ["bucket-a", "bucket-b"]
}

Surface the buckets to the user, get their choice, and re-call with --remote-path <bucket>/<path>.

download (capability-gated)

workspace_shell("connection download <name> --remote-path <provider-path> [--local-path <workspace-path>] --json")

Fetch a provider file into the workspace. Defaults to data/downloads/<name>/ if --local-path is omitted, which is the conventional landing place for fetched files. After downloading, query through DUCKDB — DuckDB reads CSV, Parquet, JSON, and Excel directly from a path.

upload (capability-gated)

workspace_shell("connection upload <name> --local-path <workspace-file> --remote-path <provider-path> --json")

Push a workspace file to a provider. Useful for round-tripping (download → process → upload), publishing generated artifacts back to shared drives, or driving recurring uploads from a scheduled job.

edit-context

workspace_shell("connection edit-context <name> <source-file> --json")

Replace a connection's RULES.md with the contents of <source-file>. Connection rules are shared with everyone who can use the connection, so they're saved through this verb rather than by editing connections/<name>/RULES.md directly — direct edits are overwritten. The source file replaces the whole context, so start from the current rules:

workspace_shell("cp connections/<name>/RULES.md connections/<name>/scratch/rules.md")
# edit connections/<name>/scratch/rules.md
workspace_shell("connection edit-context <name> connections/<name>/scratch/rules.md --json")

Only the connection's owner can update its rules from the workspace; admins can update them in the web app. connections/<name>/RULES.md shows the saved rules a few seconds later.

Scheduling with cron

Scheduled jobs use the standard Linux cron daemon that runs in the workspace pod. Jobs live in your user crontab, which is stored with your workspace and survives pod restarts.

The runner has no TTY, so your AI manages the crontab non-interactively — crontab -l to read it, and a pipe into crontab - to write it:

workspace_shell("crontab -l")
workspace_shell("""(crontab -l 2>/dev/null; echo '0 */6 * * * /usr/local/bin/connection query <name> --file connections/<name>/queries/<file>.sql --json >> /workspace/data/<file>.log 2>&1') | crontab -""")
  • The schedule is a standard 5-field cron expression (e.g., 0 */6 * * * for every six hours).
  • Common job shapes: connection query <name> --file connections/<name>/queries/<file>.sql --json, or python3 scripts/<file>.py. Jobs start in /workspace, so workspace-relative paths work.
  • Jobs run unattended with cron's minimal PATH: use absolute command paths (/usr/local/bin/connection) or put PATH=/usr/local/bin:/usr/bin:/bin on the first line of the crontab. Set non-interactive flags, and redirect output to a file in the workspace so you can review each run.
  • To remove a job, rewrite the crontab without it (for example, crontab -l | grep -v <file>.sql | crontab -).

Your workspace doesn't need to be running when a job is due: MarcoPolo starts it shortly before the scheduled time.

Self-discovery

When in doubt, ask the CLI directly:

workspace_shell("connection --help")
workspace_shell("connection <verb> --help")

These are the live source of truth for flags. Prefer them over guessing if you suspect this page may be behind the platform.

On this page