

TypeSafe Jev in Maia: Three Shared Pipelines, One Registry, One Skill.
TypeSafe's Jev model classifies, scores and routes rows against typed questions. This post shows how to expose it in Maia Foundation as a reusable capability: three versioned shared pipelines, a question-set registry, and one skill that teaches the Maia agent to compose them.
Every constraint below comes from the TypeSafe and Maia Foundation documentation, not from assumption. Where the docs are silent, we say so.
Design overview: three shared pipelines, one registry, one skill
Three versioned shared pipelines, a question-set registry, and one skill that teaches the agent to compose them.
The original hypothesis β a skill fronting a set of shared pipelines with references β is right in shape. Two corrections came out of the research, and both change the build:
A Maia skill cannot invoke anything. It is text plus bundled files. What composes the pipelines is the agent that activates the skill, using its run_pipeline tool and its documented ability to "find available shared pipelines and use them by adding Run Shared Pipeline components". So the skill is instruction, not orchestration, and it has to be unambiguous about pipeline ids and parameter names because there is no declarative binding between a skill and a pipeline.
references/, templates/ and sub-skills/ are not Maia conventions. They come from the Claude Code skill framework. Maia documents that a skill can reference files "stored in the same folder as the skill file", including pipelines and scripts, and that subfolders work β but assigns no meaning to those directory names. Use Maia's documented pattern: example pipelines and question-set JSON living in the skill's own folder.
Five Maia constraints that shape a Jev integration
1. Maia cannot put an HTTP response into a variable. There is no generic HTTP request component. The Custom Connector POSTs and lands its response as a table β "create a new table or replace an existing table for you". There is no documented path from response body to scalar variable. The integration is therefore table-oriented by force, not by choice.
2. Batching is the whole economics. Jev evaluates every question in parallel and in isolation against one state, so adding questions barely changes latency. The vendor's own benchmark puts 13 questions in one call at 11.5x cheaper and 9.6x faster than 13 calls. A design that issues one call per question is an order of magnitude wrong.
3. Two concurrency ceilings, and they nearly collide. Jev allows 1,200 requests/minute β 20 per second. Maia full SaaS allows 20 concurrent tasks (hybrid: 20 per runner instance, up to 100 instances). Whether 20 concurrent requests breaches 20/second depends entirely on call latency, which is why the concurrency cap has to be calculated rather than guessed. See the throughput section.
4. Iterators cannot read child grid results. Documented explicitly: child pipeline grid variables "are only available as a source on Run Orchestration, Run Transformation, and Run Shared Pipeline components. They don't appear on other orchestration components such as Iterator or If." So a fan-out loop cannot collect per-row answers into a grid. Per-row results must land in a table. This is the single most design-shaping sentence in the Maia corpus.
5. Iterators default to Sequential, and an unset concurrency cap means unlimited. Concurrency defaults to Sequential. Set it to Concurrent and leave Maximum concurrent iterations blank and "no upper limit will be placed on the number of concurrent tasks". Both defaults are wrong for this integration in opposite directions. Iterators also cap at 5,000 iterations and have no delay or rate-limit property β throttling is available only through the concurrency cap.
Calling the Jev API from Maia: Custom Connector vs Python Pushdown
Six candidates. Four are eliminated.
| Mechanism | Verdict | Why |
|---|---|---|
| Custom Connector | Recommended, warehouse-agnostic | POST supported; Post Body enabled for POST, JSON, ${variable} interpolation; Token property is a secret-definition drop-down. Response lands as a table |
| Python Pushdown | Recommended, Snowflake only | Real pip via Packages grid; secrets via _snowflake.get_generic_secret_string() never leave the script; own rate-limiting code; needs Snowflake NETWORK RULE + EXTERNAL ACCESS INTEGRATION |
| Python Script | Eliminated | “You can’t use the Python Script component on Full SaaS infrastructure or the Runner for Snowflake”. No secret property |
| Script Pushdown | Eliminated | “can only be used in a Hybrid SaaS deployment”. Bash variables read-only |
| Webhook Post | Eliminated | No header, auth or secret property at all, and no response capture. Fire-and-forget only |
| Snowflake external functions | Eliminated | Not documented as invocable from a Maia component. Would mean hand-written DDL in a SQL Script |
Recommendation: build the Custom Connector path first, because it works on all four warehouses and keeps credentials in a typed secret property rather than in code. Offer Python Pushdown as a Snowflake-only variant where a customer wants per-request rate limiting or in-script secret isolation.
Two Custom Connector caveats to design around. Extract Nested Data does not support BigQuery β so BigQuery customers can land the raw VARIANT but need a different unpack path. And egress comes from fixed Matillion NAT ranges which may need allow-listing; the connector also "cannot connect to self-signed HTTPS API endpoints", which is fine for api.typesafe.ai but worth recording.
Architecture: Jev shared pipelines in Maia Foundation
Four artefacts. The layering exists so that each of the three things that will change independently β the API contract, the rate-limit logic, and the output schema β changes in exactly one place.
ββββββββββββββββββββββββββββββββββββ β SKILL: system-one-evaluation β β .matillion/maia/skills/β¦ β β Teaches the agent which pipeline β β to add and how to parameterise β βββββββββββββββββ¬βββββββββββββββββββ β agent adds component βΌ ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ β QUESTION-SET REGISTRY βββββ 2. jev-evaluate-table β β table + JSON in skill β β fan-out, concurrency gate β β folder β βββββββββββββββββ¬βββββββββββββββ ββββββββββββββββββββββββ β Run Shared Pipeline βΌ ββββββββββββββββββββββββββββββββ β 1. jev-evaluate-batch β β ONE API call. Only this β β pipeline talks to TypeSafe β βββββββββββββββββ¬βββββββββββββββ β lands VARIANT βΌ JEV_RAW_RESPONSES (VARIANT) β βΌ ββββββββββββββββββββββββββββββββ β 3. jev-unpack-answers β β VARIANT β typed rows β βββββββββββββββββ¬βββββββββββββββ βΌ JEV_ANSWERS (typed)
1. jev-evaluate-batch β the primitive
The only artefact that knows the TypeSafe contract exists. One invocation, one HTTP POST, all questions batched.
Type: Orchestration. Shared pipeline id: jev-evaluate-batch
| Parameter | Type | Required | Notes |
|---|---|---|---|
| state_text | TEXT | yes | The content to evaluate |
| question_set_json | TEXT | yes | Serialised questions map. Validated by pipeline 2, not here |
| model | TEXT | yes | Default jev-1.13.0. Pin a version, never an alias |
| source_key | TEXT | yes | Business key, carried into the landing table for join-back |
| run_id | TEXT | yes | Passed down from the caller |
| target_table | TEXT | yes | Landing table for the raw VARIANT |
Returns: nothing. Side effect only β one row appended to target_table. This is forced by constraint 1 and is the right shape anyway, because it means a failed batch leaves a diagnosable artefact rather than a lost variable.
Internals: Custom Connector with method POST, Post Body built from ${state_text} and ${question_set_json}, Token bound to the secret definition, Destination the warehouse, Load Strategy append-equivalent, and a Single variant column holding the whole response.
2. jev-evaluate-table β the fan-out
Where rate limiting lives, and nowhere else.
Type: Orchestration. Shared pipeline id: jev-evaluate-table
| Parameter | Type | Required | Notes |
|---|---|---|---|
| source_table | TEXT | yes | Rows to evaluate |
| state_column | TEXT | yes | Column holding the state |
| key_column | TEXT | yes | Business key |
| question_set_name | TEXT | yes | Looked up in the registry |
| model | TEXT | no | Default jev-1.13.0 |
| max_concurrency | TEXT | yes | No default. Force the caller to decide |
| target_table | TEXT | yes | |
| run_id | TEXT | no | Generated if absent |
Internals: Table Iterator over source_table, Concurrency = Concurrent, Maximum concurrent iterations = ${max_concurrency} β explicitly set, never blank β attached to a Run Shared Pipeline calling jev-evaluate-batch.
One thing to verify before building. Maia's iterator guidance names Run Transformation and Run Orchestration as the components an iterator attaches to, and does not name Run Shared Pipeline. Whether an iterator can legally drive a Run Shared Pipeline component is not documented. If it cannot, the fallback is a thin local orchestration wrapper inside the consuming project that the iterator drives, which in turn calls the shared pipeline. Test this first β it is the highest-risk unknown in the design.
Also unverified: the docs give no nesting-depth or concurrency limit specific to shared pipelines. Do not assume unbounded.
3. jev-unpack-answers β the normaliser
VARIANT to typed rows. Isolated so schema evolution is cheap.
Type: Transformation. Shared pipeline id: jev-unpack-answers
Reads JEV_RAW_RESPONSES, uses Extract Nested Data to explode answers into one row per question evaluation, and writes JEV_ANSWERS:
| Column | Type | Notes |
|---|---|---|
| run_id | STRING | |
| evaluated_at | TIMESTAMP_TZ | |
| source_key | STRING | |
| model | STRING | From the response, the version that actually answered |
| question_id | STRING | The caller’s own key |
| question_type | STRING | noul · choice · score |
| answer | VARIANT | Noul float, chosen option, or weighted score |
| probabilities | VARIANT | Full distribution |
| confidence | FLOAT | Nullable — Noul answers carry no confidence |
| legend | VARIANT | Score only |
| input_tokens | NUMBER | Billing is input-only |
Three schema notes that come straight from the API contract. confidence must be nullable because Noul has none. score is a float that can land between levels β 1.05 in the vendor's own example β so don't cast to integer without deciding how to round. And answer is VARIANT rather than typed because the three question types return genuinely different shapes.
BigQuery: Extract Nested Data is unsupported. BigQuery customers need a SQL-based unpack; the raw landing still works.
4. Question-set registry
The thing that makes this reusable rather than a one-off. Named, versioned question sets so a caller says support_triage_v2 instead of pasting JSON.
Two halves, deliberately. A table (JEV_QUESTION_SETS: name, version, questions VARIANT, created_at, owner) as the runtime source of truth, and JSON files in the skill's folder as the human-editable, Git-versioned, reviewable copy. The skill folder copy is what a reviewer reads; the table is what the pipeline reads.
Validate on registration, not at call time: 255 options maximum per Choice, 2 to 10 levels per Score, and a token estimate against the 64k combined ceiling. A 422 at runtime names only the first offending field, which is an expensive way to find out.
Building the Maia skill that composes the Jev pipelines
Path: .matillion/maia/skills/system-one-evaluation/SKILL.md
Frontmatter carries only two documented keys. No other key appears anywhere in the Maia corpus β don't invent one.
--- name: system-one-evaluation description: > Evaluate rows of a table against typed questions using the TypeSafe Jev model, returning structured answers with probabilities and confidence. Activate when the user asks to classify, score, triage, route, rank or label rows; to detect sentiment, urgency, intent or risk; to apply a rubric; or mentions Jev, System One, TypeSafe, or confidence-gated routing. ---
Activation is model-selected from that description, or explicit via @ in the chat panel. So the description is the entire discovery mechanism β it needs the phrases a user would actually type, the action verbs, and the product names.
What the skill body must contain, because the agent has no other way to know it:
- The three pipeline ids and their exact parameter names
- The batching rule, stated as a prohibition. One call per row with all questions batched. Never one call per question
- How to look up a question set, and that inline JSON is a fallback not a default
- The output table shape, so downstream work can be written against it
- The confidence-gating pattern: the answer tells you what, confidence tells you whether to act β act, review, or escalate
- That Noul has no confidence value
- A worked example, end to end. The TypeSafe framework's own guidance is that for anything involving judgement, one worked example beats ten rules
What the skill must not attempt. Maia Team "can't create or publish shared pipelines" and "can't schedule or publish pipelines". So provisioning, publishing and scheduling are human or API work. The skill should say so explicitly, otherwise the agent will try and fail.
Rules, and a correction
Anything that must apply on every prompt goes in .matillion/maia/rules/ instead β but that folder has a strict 12,000-character limit across all files combined. Characters, not tokens.
This corrects TypeSafe Jev integration reference, which states a ~12k token budget for rules, taken from the Claude Code skill framework export. The Maia product documentation is explicit that it is 12,000 characters, which is roughly a quarter of what a token budget would allow.
Useful consequence: files outside .matillion/maia/rules/ "do not count toward the character limit". So long reference material belongs in the skill folder or an additional-context/ directory at project root, not in rules.
Maia API endpoints for running shared pipelines
There is no execute-shared-pipeline endpoint
Shared pipelines are a design-time composition primitive. To run one over the API you need a published orchestration pipeline that wraps it. That wrapper is a deliberate deliverable, not an accident.
Ship one wrapper per entry point, published to the environment: jev_evaluate_table_entry.orch.yaml β a Run Shared Pipeline call with its parameters mapped from pipeline variables.
The five endpoints to compose
| Purpose | Endpoint |
|---|---|
| Capability catalogue — resolve id to source pipeline, pin a version | GET /v1/shared-pipelines/artifacts/details |
| Execute | POST /v1/projects/{projectId}/pipeline-executions |
| Poll | GET .../pipeline-executions/{id}/status |
| Diagnose a failure across the nested tree | GET .../pipeline-executions/{id}/tasks |
| Deploy and version | POST /v1/projects/{projectId}/artifacts then /artifacts/promotions |
Execution body takes environmentName, pipelineName, optional versionName, plus scalarVariables (name to string) and gridVariables (name to two-dimensional string array). That is the typed input surface β it maps cleanly onto the shared pipeline parameters.
Use executionTag. "Only 1 pipeline execution is allowed to run per execution tag in a given environment", returning 409 on conflict. That is a free server-side mutex per environment, and for a rate-limited downstream API it is exactly the right primitive. Tag by question set, or by target table.
Maia's own API rate limit is 1,000 requests/minute with 429 and Retry-After, which is separate from and lower than Jev's 1,200.
Gaps that shape the design
No skills API. A grep across the whole API reference returns zero files mentioning skills. Skills cannot be created, listed, read or validated over the API β the only distribution channel is Git. "Publish this skill to N customer projects" has to be repo automation, not API calls.
No way to bind a skill to an API-created agent task. POST /v1/ai/agents/tasks takes a message, branch, environment and optionally a graph β no skill parameter. Project skills probably load from the branch, since sourceBranchName is required, but the docs never say so. Verify empirically before designing around it.
shared-pipelines.yaml is inaccessible on Maia Hosted Git. Sharing happens in Designer and writes that file into the repo's .matillion folder. For hosted-Git customers, fully automated provisioning is blocked. This needs saying to anyone planning a self-serve rollout.
GET /v1/shared-pipelines returns results[] typed any β the cross-namespace catalogue has no documented schema. Use /artifacts/details instead, which is typed.
resolver_ref format is undocumented, so a pipeline secret reference cannot be constructed blind. Call POST /v1/secrets/{agentId}/secret-value and read it back.
No webhooks anywhere. Execution status, task messages and consumption are all poll-only, and consumption refreshes every three hours. Budget for a poller.
No telemetry dimension for skill activation. /v1/consumption dimensions are pipeline-shaped. "Did anyone use the skill" is not answerable from telemetry β the nearest proxies are GET /v1/events and listing agent tasks by project and date. Worth knowing before promising adoption metrics.
Rate limits and concurrency: Jev vs Maia
Set concurrency from measured latency rather than picking a number:
max_concurrency = target_requests_per_second Γ observed_latency_seconds
Against Jev's 1,200/minute, a safe target is 15/second, leaving headroom because the vendor states the limits "can change without notice".
| Observed latency | Concurrency for ~15 rps | Against Maia’s 20-task cap |
|---|---|---|
| 0.5 s | 8 | comfortable |
| 1.0 s | 15 | near the cap |
| 2.0 s | 30 | exceeds full SaaS — needs hybrid with 2+ runners |
So on full SaaS the platform's 20-task ceiling becomes the binding constraint once latency passes about 1.3 seconds. Hybrid gives 20 per runner instance up to 100 instances, which is the route to real scale.
Two other ceilings: iterators cap at 5,000 iterations per run, so larger tables need chunking upstream. And the 64k token budget covers state plus all questions combined, so a large question set shrinks the state each row can carry.
Keeping the Jev API key secret in Maia
The API key goes in a secret definition, consumed via the Custom Connector's Token drop-down. Credentials are not stored in the connector β "The custom connector only saves the authentication type."
What must never hold the key, all documented as exposed:
- Any scalar or grid variable. Print Variables exists as a component and dumps values to the task log. Python print and Bash echo output become the task completion message
- Custom connector Post Body, query params and header params, which accept ${variable} β a token passed that way becomes a variable and inherits every exposure above. Use the Token property
- shared-pipelines.yaml β parameter default values are committed to Git in plaintext
- Post-processing and execution metadata β ${sysvar.childPipeline.vars.*} propagates a child's variable values to the parent, and custom output messages can map variables into run history
Hybrid customers manage their own AWS Secrets Manager or Azure Key Vault; full SaaS uses Matillion's managed vault. Note the documented polarity trap: private keys should be stored as plaintext at environment level despite AWS warning about it, while component-level usage wants key/value JSON with escaped \n.
Not documented anywhere: whether variable values are redacted in data lineage events. Assume they are not.
Open questions to verify before you build
Ordered by how much a wrong answer costs.
- Can an iterator attach to a Run Shared Pipeline component? Undocumented. If not, pipeline 2 needs a local wrapper inside each consuming project, which materially weakens the "account-level reusable capability" story.
- Do project skills load into an API-created agent task from sourceBranchName? Undocumented. Determines whether this works headlessly or only in Designer.
- Shared pipeline nesting depth and concurrency limits. Silent in the corpus.
- Jev call latency against representative state sizes. Sets the concurrency figure and nothing else can.
- resolver_ref format, by calling the endpoint once.
- Whether requests is available in the Python Pushdown Anaconda channel, if the Snowflake variant gets built. boto3, pandas, numpy, certifi and httplib2 are confirmed present; requests was not visible in the portion of the package table read.
Build sequence: seven stages from proof of call to API wrapper
Stage 1 β prove the call. Custom Connector against POST /v1/systemone, one hardcoded state, one Noul question, secret-bound token, response landing to a VARIANT column. Nothing else. This retires the largest technical risk in a day.
Stage 2 β jev-evaluate-batch as a shared pipeline, parameterised, published, version-pinned.
Stage 3 β resolve verification item 1, then build jev-evaluate-table with an explicit concurrency cap.
Stage 4 β jev-unpack-answers and the typed output table. Snowflake first; BigQuery needs the SQL path.
Stage 5 β the registry, with validation on registration.
Stage 6 β the skill, written last, once the pipeline ids and parameter names are stable. A skill written against a moving contract is worse than no skill, because the agent follows it confidently.
Stage 7 β the wrapper orchestration pipeline and the API composition, if headless execution is needed.
Fund a review slot before stage 6. The skill is the artefact most likely to drift out of step with the pipelines, and a stale skill fails silently β the agent does what it was told, against parameters that have changed.
Data management made effortless

Related Resources
Data management


.png)
