> ## Documentation Index > Fetch the complete documentation index at: https://openrouter.ai/docs/llms.txt > Use this file to discover all available pages before exploring further. # Next Turn Params > Build encapsulated, context-aware tools with `nextTurnParams`. Create skills systems, plugins, and adaptive multi-turn agents. ## Why nextTurnParams? Traditional tool execution returns results to the model, but sometimes you need more: * **Skills/Plugins**: Load domain-specific instructions when a skill is activated * **Progressive Context**: Build up context as tools are used * **Adaptive Behavior**: Adjust model parameters based on tool results * **Clean Separation**: Tools manage their own context requirements With `nextTurnParams`, tools can modify any `callModel` parameter for the next turn. ## Basic Example ```typescript expandable lines theme={null} import { tool } from '@openrouter/agent'; import { z } from 'zod'; const expertModeTool = tool({ name: 'enable_expert_mode', description: 'Enable expert mode for detailed technical responses', inputSchema: z.object({ domain: z.string().describe('Technical domain (e.g., "kubernetes", "react")'), }), outputSchema: z.object({ enabled: z.boolean() }), nextTurnParams: { instructions: (params, context) => { const base = context.instructions ?? ''; return `${base} EXPERT MODE ENABLED for ${params.domain}: - Provide detailed technical explanations - Include code examples and best practices - Reference official documentation - Assume advanced knowledge`; }, temperature: () => 0.3, // More precise for technical content }, execute: async (params) => { return { enabled: true }; }, }); ``` ## The Claude Code Skills Pattern This example shows how to recreate Claude Code's skills system as a single encapsulated tool: ```typescript expandable lines theme={null} import { tool } from '@openrouter/agent'; import { readFileSync } from 'fs'; import { z } from 'zod'; const skillsTool = tool({ name: "skill", description: `Load a specialized skill to enhance the assistant's capabilities. Available skills: pdf-processing, data-analysis, code-review, etc. Each skill provides domain-specific instructions and capabilities.`, inputSchema: z.object({ type: z.string().describe("The skill type to load (e.g., 'pdf-processing')"), }), outputSchema: z.string(), // nextTurnParams runs after all tool calls execute, before responses go to model // Executed in order of tools array. This is where the magic happens. nextTurnParams: { input: (params, context) => { // Prevent duplicate skill loading if (JSON.stringify(context.input).includes(`Skill ${params.type} is already loaded`)) { return context.input; } // Load the skill's instructions from file system const skill = readFileSync( `~/.claude/skills/${params.type}/SKILL.md`, "utf-8" ); // Inject skill context into the conversation return [ ...context.input, { role: "user", content: `Base directory for this skill: ~/.claude/skills/${params.type}/ ${skill}`, }, ]; }, }, execute: async (params, context) => { // Check if already loaded if (JSON.stringify(context.input).includes(`Skill ${params.type} is already loaded`)) { return `Skill ${params.type} is already loaded`; } return `Launching skill ${params.type}`; }, }); // Usage - the skill automatically enriches future turns const result = openrouter.callModel({ model: 'anthropic/claude-sonnet-4.5', input: 'Process this PDF and extract the key findings', tools: [skillsTool], }); ``` ### Key Benefits 1. **Encapsulation**: Skill loading logic is entirely contained in the tool 2. **Idempotency**: Built-in check prevents loading the same skill twice 3. **Clean API**: Callers don't need to know about skill file locations 4. **Composability**: Multiple skills can be loaded across turns ## Execution Order Understanding when `nextTurnParams` runs is crucial: ```lines theme={null} 1. Model generates tool calls ↓ 2. All tool `execute` functions run ↓ 3. `nextTurnParams` functions run for each tool (in tools array order) ↓ 4. Modified parameters used for next model turn ↓ 5. Repeat until model stops calling tools ``` ## Available Context `nextTurnParams` functions receive two arguments: ### params The validated input parameters that were passed to the tool: ```typescript lines theme={null} nextTurnParams: { instructions: (params, context) => { // params is typed based on inputSchema console.log(params.type); // e.g., "pdf-processing" return `Handle ${params.type}`; }, }, ``` ### context The current request context, including: | Property | Type | Description | | ----------------- | ----------------------- | --------------------------- | | `input` | `OpenResponsesInput` | Current message history | | `model` | `string \| undefined` | Current model selection | | `models` | `string[] \| undefined` | Model fallback array | | `instructions` | `string \| undefined` | Current system instructions | | `temperature` | `number \| undefined` | Current temperature | | `maxOutputTokens` | `number \| undefined` | Current max tokens | | `topP` | `number \| undefined` | Current top-p sampling | | `topK` | `number \| undefined` | Current top-k sampling | ## Modifiable Parameters You can modify `CallModelInput` parameters: ```typescript lines theme={null} nextTurnParams: { // Modify message history input: (params, ctx) => [...ctx.input, newMessage], // Change model model: (params, ctx) => 'anthropic/claude-sonnet-4.5', // Update instructions instructions: (params, ctx) => `${ctx.instructions}\n\nNew context...`, // Adjust generation parameters temperature: (params, ctx) => 0.5, maxOutputTokens: (params, ctx) => 2000, }, ``` ## Patterns ### Research Context Accumulation Build up context as research progresses: ```typescript lines theme={null} const researchTool = tool({ name: "research", inputSchema: z.object({ topic: z.string() }), outputSchema: z.object({ findings: z.array(z.string()) }), nextTurnParams: { instructions: (params, context) => { const base = context.instructions ?? ''; return `${base} Previous research on "${params.topic}" found important context. Build upon these findings in your response.`; }, }, execute: async (params) => { const results = await searchDatabase(params.topic); return { findings: results }; }, }); ``` ### Complexity-Based Model Selection Upgrade to better models when needed: ```typescript expandable lines theme={null} const complexityAnalyzer = tool({ name: "analyze_complexity", inputSchema: z.object({ code: z.string() }), outputSchema: z.object({ complexity: z.enum(['low', 'medium', 'high']) }), nextTurnParams: { model: (params, context) => { // Upgrade to more capable model for complex code if (params.complexity === 'high') { return 'anthropic/claude-sonnet-4.5'; } return context.model ?? 'openai/gpt-5-nano'; }, temperature: (params, context) => { // Lower temperature for complex analysis return params.complexity === 'high' ? 0.3 : 0.7; }, }, execute: async (params) => { return analyzeCodeComplexity(params.code); }, }); ``` ### Multi-Skill Loading Load multiple skills at once: ```typescript expandable lines theme={null} const multiSkillLoader = tool({ name: 'load_skills', description: 'Load multiple skills at once', inputSchema: z.object({ skills: z.array(z.string()).describe('Array of skill names to load'), }), outputSchema: z.object({ loaded: z.array(z.string()), failed: z.array(z.object({ name: z.string(), reason: z.string() })), }), nextTurnParams: { input: (params, context) => { let newInput = context.input; for (const skillName of params.skills) { const skillPath = `~/.skills/${skillName}/SKILL.md`; if (!existsSync(skillPath)) continue; const skillMarker = `[Skill: ${skillName}]`; if (JSON.stringify(newInput).includes(skillMarker)) continue; const skillContent = readFileSync(skillPath, 'utf-8'); newInput = [ ...(Array.isArray(newInput) ? newInput : [newInput]), { role: 'user', content: `${skillMarker}\n${skillContent}` }, ]; } return newInput; }, }, execute: async ({ skills }) => { const loaded = []; const failed = []; for (const skill of skills) { if (existsSync(`~/.skills/${skill}/SKILL.md`)) { loaded.push(skill); } else { failed.push({ name: skill, reason: 'Not found' }); } } return { loaded, failed }; }, }); ``` ### Language/Locale Switching Adapt to user language preferences: ```typescript expandable lines theme={null} const languageTool = tool({ name: 'set_language', inputSchema: z.object({ language: z.enum(['en', 'es', 'fr', 'de', 'ja']), }), outputSchema: z.object({ set: z.boolean() }), nextTurnParams: { instructions: (params, context) => { const base = context.instructions ?? ''; const languageInstructions = { en: 'Respond in English.', es: 'Responde en español.', fr: 'Répondez en français.', de: 'Antworten Sie auf Deutsch.', ja: '日本語で回答してください。', }; return `${base}\n\n${languageInstructions[params.language]}`; }, }, execute: async (params) => ({ set: true }), }); ``` ## Best Practices ### Idempotency Checks Always check if context was already added: ```typescript lines theme={null} nextTurnParams: { input: (params, context) => { const marker = `[Context: ${params.id}]`; // Don't add if already present if (JSON.stringify(context.input).includes(marker)) { return context.input; } return [...context.input, { role: 'user', content: `${marker}\n${newContent}`, }]; }, }, ``` ### Type Safety Use proper typing for context access: ```typescript lines theme={null} nextTurnParams: { instructions: (params, context) => { // Safe access with fallback const base = context.instructions ?? 'You are a helpful assistant.'; return `${base}\n\nAdditional context: ${params.data}`; }, }, ``` ### Minimal Modifications Only modify what's necessary: ```typescript lines theme={null} // Good: Minimal, targeted change nextTurnParams: { temperature: (params) => params.needsPrecision ? 0.2 : undefined, }, // Avoid: Unnecessary spreading nextTurnParams: { temperature: (params, ctx) => { return params.needsPrecision ? 0.2 : ctx.temperature; }, }, ``` ## See Also * **[Skills Loader Example](/docs/agent-sdk/call-model/examples/skills-loader)** - Complete implementation * **[Dynamic Parameters](/docs/agent-sdk/call-model/dynamic-parameters)** - Async parameter functions * **[Stop Conditions](/docs/agent-sdk/call-model/stop-conditions)** - Execution control