Using Attachments
Pass images into your flow alongside the message — vision input for multimodal LLM blocks.
Attachments let you feed images into your flow alongside message. Each LLM block that uses a vision-capable model can read both the text and the image when generating its output.
This guide covers what's supported, how to host the file, how to send the request, and how to handle errors.
How It Works
Noukai is stateless w.r.t. attachment bytes. You host the file on any HTTPS-reachable URL — your CDN, S3 bucket, Supabase storage, a public asset host, etc. — and pass the URL through. Noukai validates the URL syntax, checks the MIME type against an allowlist, verifies the chosen model supports it, then forwards the URL straight to the LLM provider (OpenRouter).
What this means:
- You own hosting. Noukai never receives, stores, or proxies the bytes.
- The URL must be publicly reachable by OpenRouter's fetcher. A signed URL works as long as it's still valid when the request runs.
- No upload step. There's no Noukai endpoint to POST a file to — you point at a file you've already hosted.
What's Supported
v1 supports images only via URL passthrough.
| MIME type | Notes |
|---|---|
image/jpeg | |
image/png | |
image/webp | |
image/gif | Most models read the first frame only |
| Limit | Value |
|---|---|
| Max attachments per request | 10 |
| URL scheme | https:// only |
| Max URL length | 2048 chars |
The Attachment Object
Every attachment is a JSON object with this shape:
| Field | Required | Description |
|---|---|---|
kind | Yes | Discriminator. Must be "url". |
url | Yes | HTTPS URL to the image. |
mime_type | Yes | MIME type from the supported list. |
filename | No | Display name for run history (≤ 255 chars, no /, \, or ..). |
Walkthrough
Pick a vision-capable model on the receiving block
Open your flow in app.noukai.xyz and set the LLM block's model to one that accepts images:
google/gemini-2.0-flash-001anthropic/claude-sonnet-4-6openai/gpt-4oopenai/gpt-4o-mini
If a block's model doesn't support images and you send an image attachment, the request is rejected with 400 MODEL_MIME_INCOMPATIBLE before execution starts.
Reference the attachment in your prompt
Inside the block's prompt, write naturally — the model receives the image alongside the prompt text automatically. You don't need a placeholder for the image itself.
Host the image somewhere HTTPS-reachable
Examples:
- Public S3 bucket / CloudFront distribution
- Supabase storage with a public bucket or signed URL
- Cloudflare R2 / Backblaze B2 with public access
- Your existing app's CDN
The URL must work when fetched by OpenRouter's outbound network — private IPs, localhost, and intranet hosts are blocked at validation time.
Code Examples
Multiple Attachments
You can attach up to 10 files in one request. All blocks in the flow see the same attachments — there is no per-block routing in v1.
Non-LLM blocks (code, passthrough) receive the attachments array on BlockContext.initial.attachments but ignore it by default — only LLM blocks send the images to the model.
Async Execution
Attachments work identically with the async /jobs endpoint. The request body is the same; the response is 202 with an executionId, and the attachments are persisted on the run record so polling and run-history rendering see them.
Error Handling
Validation runs before any execution work, so attachment problems fail fast with a structured 400:
| Code | Fix |
|---|---|
PARAMETER_NAME_RESERVED | You put attachments inside parameters. Move it to the top level. |
ATTACHMENT_LIMIT_EXCEEDED | Send ≤ 10 attachments per request. |
ATTACHMENT_INVALID_SCHEME | Use https://. http://, data:, and blob: are rejected. |
ATTACHMENT_BLOCKED_HOST | Hostname resolved to a private/loopback IP. Use a public CDN. |
ATTACHMENT_URL_TOO_LONG | Trim the URL or use a shorter signed-URL strategy. |
ATTACHMENT_UNSUPPORTED_MIME | Check the supported MIME table above. Convert HEIC/AVIF/etc. before uploading. |
ATTACHMENT_INVALID_FILENAME | Strip path separators (/, \, ..) and keep filename ≤ 255 chars. |
MODEL_MIME_INCOMPATIBLE | Switch the named block's model to a vision-capable one. The error includes step_id, model, and unsupported_mime. |
A complete reference (including CAPABILITY_REGISTRY_UNAVAILABLE) lives in the Slug Execution API Reference.
The image URL must still be live when the model fetches it. Signed URLs that have expired between when you sent the Noukai request and when the model dispatches will surface as a runtime error from OpenRouter. Use long-enough TTLs or public URLs for production traffic.
Privacy and Security
- Bytes are never stored by Noukai. Only the URL string and metadata are persisted on the run record.
- SSRF is blocked. Private/loopback/link-local hostnames are rejected at the API boundary; the validator re-resolves the hostname and checks against blocked CIDR ranges.
- No HEAD pre-check. Noukai does not pre-fetch your URL to verify reachability — that would add latency and create a denial-of-service vector for the customer's CDN. Liveness is verified at model-dispatch time.
Common Patterns
Vision-driven classification
Pair with an output schema like {"category": "string", "confidence": "number"} on the LLM block to get structured output.
Multi-image diff/comparison
Attach two images and ask the model to compare them. Works well for QA screenshots, before/after photos, design review.
User-uploaded media in your app
Workflow:
- User uploads an image to your storage (S3 / Supabase / R2).
- Your backend gets back the public URL (or signs one with a 30-minute TTL).
- Your backend calls Noukai with the URL in
attachments. - Noukai returns the model's structured output.
No additional Noukai endpoint, no separate "upload" step.
Next Steps
- Slug Execution API — full request/response spec and every error code
- Calling Your Flow — sync vs async, polling, version pinning
- Pipeline Recipes — end-to-end flow examples