railtownai/railtracks/middleware
Claude Code and agent skill by railtownai/railtracks
Trust tier 2
- community
Source check pending
Checking that the SKILL.md file is still in the repository.
Add middleware to a railtracks agent or node (retries, logging, timing, guardrails, context injection). Use when the user wants to wrap, retry, guard, or observe node or LLM calls.
Install
npx vskill@latest install railtownai/railtracks/middlewareSKILL.md
Add Middleware to a Railtracks Agent or Node
The user wants to add middleware to a railtracks node/agent: $ARGUMENTS
Quick reference
| Decorator | Signature | Slot | Fires on failure? | |
|---|---|---|---|---|
rt.wrap_node | async fn(call, *args, **kwargs) -> result | middleware= or model_middleware= | Yes — you control the try/except | |
rt.wrap_llm | async fn(llm_call, message_history, schema, tools) -> Response | model_middleware= only | Yes | |
rt.pre_llm | fn(message_history, schema, tools) -> (message_history, schema, tools) (sync or async) | model_middleware= only | N/A (runs before the call) | |
rt.post_llm | fn(response) -> response (sync or async) | model_middleware= only | No — skipped on exception | |
rt.post_node | fn(result) -> result (sync or async) | middleware= or model_middleware= | No — skipped on exception |
- All five accept optional keyword-only
name=and work bare or called:@rt.pre_llm,@rt.post_node(name="print_after"). wrap_node/wrap_llmrequireasync def(TypeErrorotherwise).pre_llm/post_llm/post_nodeaccept sync or async.middleware=wraps the whole node call (an agent's full tool-calling loop, including every tool node it invokes).model_middleware=wraps one raw LLM call inside that loop.- Attach at creation —
rt.agent_node(..., middleware=[...], model_middleware=[...])— or after the fact without mutating the original —rt.couple(node, middleware=[...], model_middleware=[...]). - List order = wrapping order:
middleware=[A, B, C]→AwrapsBwrapsCwraps the node.rt.couple-ing more onto an already-middlewared node adds the new ones outermost; existing middleware stays innermost, closest to the node.
Best practices
1. Exception handling & retries
pre_llm,post_llm,post_nodeonly run on success — an exception skips them entirely. If you need to react to failure (cleanup, translation, fallback), usewrap_node/wrap_llmand put your owntry/exceptaroundawait call(...).- Where you put a retry changes what it re-executes:
model_middleware=retry only re-issues the raw LLM call — safe by default, no side effects beyond the model provider request.middleware=retry on an agent re-runs the entire node, including every tool call that already succeeded in that attempt. Only retry at this slot if the tools involved are safe to re-run (idempotent, or guarded by an idempotency key) — otherwise a transient failure after a non-idempotent tool call (e.g. "send email", "charge card") causes it to fire twice.- Default to retrying in
model_middleware=for provider flakiness (rate limits, timeouts). Reach formiddleware=retry only when you've confirmed the tools are safe to repeat. - Prefer
railtracks.prebuilt.middleware.Retry(max_tries=..., approach=..., retry_on=...)over hand-rolling — it raises once attempts are exhausted rather than swallowing the failure, and lets you scoperetry_onto specific exception types instead of catching everything. - Don't swallow exceptions silently inside a
wrap_node/wrap_llmmiddleware (except Exception: return default) — that hides the failure from callers and from anything else in the chain. If you must return a fallback, do it deliberately (log it, tag it in the result) rather than as an unannounced side effect.
2. Middleware vs. guardrails
rt.input_guard/rt.output_guardare middleware under the hood (BaseGuardrailextendsMiddleware), specialized for one thing: "should this content pass?" The guard function receives anLLMGuardrailEventand returns aGuardrailDecision(allow/block/transform).- Use a guard, not hand-rolled middleware, whenever the logic is a content-policy decision (PII/secrets detection, profanity, jailbreak checks, redaction). You get for free: structured
GuardrailTracerecords (rail_name,phase,action,reason),fail_open=to decide whether a guard's own bug blocks or passes traffic, and consistent block semantics other tooling can rely on. - To keep a domain-specific agent from answering off-topic requests, use the prebuilt
ScopeGuard(scope=..., model=..., sensitivity=...)fromrailtracks.prebuilt.guardrails; a judge model labels the request before the agent's own model runs. - Use plain middleware for everything that isn't a pass/block decision on content: retries, timing, logging, context injection, auth/telemetry.
3. Ordering & composition
- Two different things happen depending on middleware shape — know which one you're ordering:
- Input-transforming middleware (
pre_llm-style: mutatesmessage_history/schema/toolsbefore calling onward) — earlier entries in the list run first and hand their result to later entries. Put transformations before validations:model_middleware=[ContextInjection(), my_guard]so the guard sees the filled-in prompt, not the raw template. - Span-wrapping middleware (retry, timing, logging that brackets a whole call) — the outermost entry sees the aggregate outcome; the innermost is what actually gets re-invoked on each attempt.
middleware=[Retry(3), log_after]→ the log fires once per retry attempt (it's inside the retry).middleware=[log_after, Retry(3)]→ the log fires once, for the final outcome only (it's outside the retry). Pick based on whether you want per-attempt or per-call visibility. - When
rt.couple-ing middleware onto a node that already has some, the new list is outermost — it runs before the existing middleware, so it can't be placed inside an existing auth check or retry.
4. Naming & observability
name=setsMiddleware.name, which is recorded with the middleware's events and shown in the beta visualizer (railtracks viz --beta) — the same way guardrails write theirnameinto everyGuardrailTrace.rail_name. Give your middleware a realname=so it's identifiable in traces/logs instead of falling back to the wrapper function's__name__.- Set
name=when the same wrapper function is reused in more than one slot (e.g. one genericretry_nodeattached to several agents) and you need to tell instances apart. - Keep one concern per middleware function. A function that both logs and mutates the prompt makes the ordering question in §3 ambiguous — split it into two so each one has one clear rule for where it belongs in the list.
5. State
- A middleware instance is shared, never copied: every node it is attached to (both slots), every node
rt.couplebuilds from those, and every run, including concurrent runs, call the same object. - Keep per-run state in
rt.contextunder a key the middleware owns (one key per instance, e.g. auuidsuffix), asConversationMemorydoes. Each run gets its own copy of the context dictionary, so concurrent runs stay apart. - Keep only process-wide resources on
selfor in a closure: clients, connection pools, semaphores. - For a budget, use
railtracks.prebuilt.middleware.MaxCalls, which keeps a separate count per session. To serialize calls that share state, putrailtracks.prebuilt.middleware.Lockoutside the middleware that uses it.
Patterns
Retry: prebuilt vs. hand-rolled, and where to put it
Guardrail vs. plain middleware for the same-shaped problem
Running a guarded agent
Run it through a Flow like any agent. A block raises GuardrailBlockedError, so catch it where you want to show the user a refusal.
Ordering a retry against a logger to control per-attempt vs. per-call visibility
Attaching middleware after the node already exists
Rules
- Retry at
model_middleware=by default. Retry atmiddleware=only when every tool the agent calls is safe to run twice, since a retry there re-runs every tool call in the attempt. - Write content allow/block checks as
input_guard/output_guard, so they show up inGuardrailTracewith the rest of your rails. - Write
wrap_node/wrap_llmfunctions asasync def. - Put failure-aware logic in
wrap_llm/wrap_node;post_llm/post_noderun only on success. - Keep per-run middleware state in
rt.context, not onself; one instance serves every run, including concurrent ones.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingpackages/railtracks/src/railtracks/cli/skills/middleware/SKILL.md
Popularity
- Repository stars
- 234
- Repository forks
- 67
- 7-day trend score
- 5
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/railtownai/railtracks/middleware)