From 13ce631f53e304798589b9674b6a9419f8da93d4 Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Thu, 27 Aug 2026 12:42:29 +0300
Subject: [PATCH 01/14] docs(blog): add client generator intro blog post as a
React page
- Add /blog/agent-friendly-sdks as a React page with inline hero diagram
and a reviewer-style CTA card
- Support react-frontmatter for .page.tsx blog posts in the theme plugin
- Add blog-recent-posts shared data (top 4) and a shared RecentPosts
component that excludes the currently open post; localize the BlogPost
template to use it
- Add api-descriptions:openapi and api-lifecycle:sdks blog categories
Co-Authored-By: Claude Fable 5
---
@theme/components/Blog/RecentPosts.tsx | 52 ++
@theme/plugin.js | 32 +-
@theme/templates/BlogPost.tsx | 64 +-
@theme/utils/blog-post.js | 34 +-
blog/agent-friendly-sdks.page.tsx | 949 +++++++++++++++++++++++++
blog/images/cta-eclipse.svg | 16 +
blog/metadata/blog-metadata.yaml | 8 +
7 files changed, 1131 insertions(+), 24 deletions(-)
create mode 100644 @theme/components/Blog/RecentPosts.tsx
create mode 100644 blog/agent-friendly-sdks.page.tsx
create mode 100644 blog/images/cta-eclipse.svg
diff --git a/@theme/components/Blog/RecentPosts.tsx b/@theme/components/Blog/RecentPosts.tsx
new file mode 100644
index 000000000..fa1c99e12
--- /dev/null
+++ b/@theme/components/Blog/RecentPosts.tsx
@@ -0,0 +1,52 @@
+import * as React from 'react';
+import styled from 'styled-components';
+
+import { useThemeHooks } from '@redocly/theme/core/hooks';
+
+import { ArticleCard } from '@redocly/marketing-pages/components/Blog/RecentPosts.js';
+import { H2Title } from '@redocly/marketing-pages/components/TypographyElements/TypographyElements.js';
+
+type RecentPost = { slug: string; title: string; description?: string };
+
+// Same layout as marketing-pages RecentPosts, but reads the deeper 'blog-recent-posts'
+// shared data and filters out the post it renders on, so a post never lists itself.
+export function RecentPosts({ currentSlug }: { currentSlug?: string }) {
+ // @ts-ignore
+ const { usePageSharedData } = useThemeHooks();
+ const recentPosts = usePageSharedData('blog-recent-posts') ?? [];
+
+ const posts = recentPosts.filter((post) => post.slug !== currentSlug).slice(0, 3);
+
+ if (posts.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+ Latest from our blog
+
+
+ {posts.map((post) => (
+
+ ))}
+
+ >
+ );
+}
+
+const RecentPostsGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-gap: 5rem;
+ justify-items: center;
+
+ @media screen and (min-width: 900px) {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+`;
diff --git a/@theme/plugin.js b/@theme/plugin.js
index 5f3484125..4180912b0 100644
--- a/@theme/plugin.js
+++ b/@theme/plugin.js
@@ -7,6 +7,7 @@ const ABOUT_SLUG = '/about/';
const BLOG_METADATA_PATH = 'blog/metadata/blog-metadata.yaml';
const LATEST_POSTS_SHARED_DATA_ID = 'blog-latest-posts';
+const RECENT_POSTS_SHARED_DATA_ID = 'blog-recent-posts';
const ALL_POSTS_SHARED_DATA_ID = 'blog-posts';
function __dirname(url) {
@@ -26,11 +27,11 @@ export default function themePlugin() {
// Register preview route for the editor iframe
const previewTemplateId = actions.createTemplate(
'preview-template',
- fromCurrentDir(import.meta.url, './preview.route.tsx')
+ fromCurrentDir(import.meta.url, './preview.route.tsx'),
);
const blogTemplateId = actions.createTemplate(
- 'blog-template',
- fromCurrentDir(import.meta.url, './blog.page.tsx')
+ 'blog-template',
+ fromCurrentDir(import.meta.url, './blog.page.tsx'),
);
actions.addRoute({
excludeFromSidebar: true,
@@ -58,7 +59,7 @@ export default function themePlugin() {
templateId: blogTemplateId,
hasClientRoutes: true,
});
-
+
const metadataContentRecord = await context.cache.load(BLOG_METADATA_PATH, 'yaml');
const categories = metadataContentRecord.data.categories || [];
@@ -84,10 +85,11 @@ export default function themePlugin() {
// Existing blog data processing
const postRoutes = actions
.getAllRoutes()
- .filter((route) =>
- route.slug.startsWith(BLOG_SLUG) &&
- route.slug !== BLOG_SLUG &&
- !route.slug.startsWith('/blog/category/')
+ .filter(
+ (route) =>
+ route.slug.startsWith(BLOG_SLUG) &&
+ route.slug !== BLOG_SLUG &&
+ !route.slug.startsWith('/blog/category/'),
);
const categoryRoutes = actions
@@ -98,8 +100,14 @@ export default function themePlugin() {
const latestPosts = postsData.posts.slice(0, 3);
+ // Top 4 posts, so a post page can exclude itself and still show 3 recent posts
+ const recentPosts = postsData.posts
+ .slice(0, 4)
+ .map(({ slug, title, description }) => ({ slug, title, description }));
+
// Create shared data for blog pages
await actions.createSharedData(LATEST_POSTS_SHARED_DATA_ID, latestPosts);
+ await actions.createSharedData(RECENT_POSTS_SHARED_DATA_ID, recentPosts);
await actions.createSharedData(ALL_POSTS_SHARED_DATA_ID, postsData);
// Add latest posts shared data to all blog posts and update metadata
@@ -110,11 +118,17 @@ export default function themePlugin() {
LATEST_POSTS_SHARED_DATA_ID,
);
+ actions.addRouteSharedData(
+ post.slug,
+ RECENT_POSTS_SHARED_DATA_ID,
+ RECENT_POSTS_SHARED_DATA_ID,
+ );
+
const postRoute = actions.getRouteBySlug(post.slug);
postRoute.metadata = { ...postRoute.metadata, ...post };
}
-
+
// Add all posts shared data to category routes
for (const categoryRoute of categoryRoutes) {
actions.addRouteSharedData(
diff --git a/@theme/templates/BlogPost.tsx b/@theme/templates/BlogPost.tsx
index cde0630f7..5e4db51f6 100644
--- a/@theme/templates/BlogPost.tsx
+++ b/@theme/templates/BlogPost.tsx
@@ -1,3 +1,63 @@
-import Page from '@redocly/marketing-pages/templates/BlogPost.js';
+import React from 'react';
+import styled from 'styled-components';
-export default Page;
\ No newline at end of file
+import type { Post } from '@redocly/marketing-pages/components/Blog/types.js';
+
+import { useThemeHooks } from '@redocly/theme/core/hooks';
+import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
+import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js';
+import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js';
+import { Box } from '@redocly/marketing-pages/ui/Box.js';
+
+import { RecentPosts } from '../components/Blog/RecentPosts';
+
+// Local version of @redocly/marketing-pages/templates/BlogPost.js — the only
+// difference is the RecentPosts section, which excludes the currently open post.
+export default function BlogPost(props: { children?: React.ReactNode }) {
+ const { usePageProps } = useThemeHooks();
+ const pageProps = usePageProps();
+
+ const { publishedDate, author, categories, title, image, slug } = pageProps.metadata as Post & {
+ slug?: string;
+ };
+
+ return (
+
+
+
+
+ {props.children}
+
+
+
+
+
+
+
+
+ );
+}
+
+const BlogMediaBox = styled.div`
+ margin-left: auto;
+ margin-right: auto;
+ max-width: calc(90vw);
+
+ @media screen and (min-width: 900px) {
+ max-width: 800px;
+ }
+`;
+
+const PageWrapper = styled.div`
+ position: relative;
+ overflow: hidden;
+`;
diff --git a/@theme/utils/blog-post.js b/@theme/utils/blog-post.js
index 8ed216162..563b97973 100644
--- a/@theme/utils/blog-post.js
+++ b/@theme/utils/blog-post.js
@@ -10,11 +10,19 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
const metadata = await transformMetadata(metadataContentRecord.data, context.fs.cwd, outdir);
for (const route of postRoutes) {
- const {
- data: { content, frontmatter },
- } = await context.cache.load(route.fsPath, 'markdown-frontmatter');
+ // React blog posts export `frontmatter`; markdown posts use YAML frontmatter
+ const isReactPage = /\.page\.tsx?$/.test(route.fsPath);
+ const { data } = await context.cache.load(
+ route.fsPath,
+ isReactPage ? 'react-frontmatter' : 'markdown-frontmatter',
+ );
+ const frontmatter = isReactPage ? data : data?.frontmatter;
- if (frontmatter?.ignore === true || (await context.isPathIgnored(route.fsPath))) {
+ if (
+ (isReactPage && !frontmatter) ||
+ frontmatter?.ignore === true ||
+ (await context.isPathIgnored(route.fsPath))
+ ) {
continue;
}
@@ -26,15 +34,15 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
.map((categoryId) => {
const categoryData = metadata.categories.get(categoryId);
if (!categoryData) return null;
-
+
if (categoryData.category && categoryData.subcategory) {
- return categoryData;
+ return categoryData;
} else {
- return {
+ return {
category: {
- id: categoryData.id,
- label: categoryData.label
- }
+ id: categoryData.id,
+ label: categoryData.label,
+ },
};
}
})
@@ -72,12 +80,12 @@ async function transformMetadata(metadata, cwd, outdir) {
categories.set(fullId, {
category: {
id: category.id,
- label: category.label
+ label: category.label,
},
subcategory: {
id: subcategory.id,
- label: subcategory.label
- }
+ label: subcategory.label,
+ },
});
}
}
diff --git a/blog/agent-friendly-sdks.page.tsx b/blog/agent-friendly-sdks.page.tsx
new file mode 100644
index 000000000..557d64847
--- /dev/null
+++ b/blog/agent-friendly-sdks.page.tsx
@@ -0,0 +1,949 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { useThemeHooks } from '@redocly/theme/core/hooks';
+import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
+
+import type { Post } from '@redocly/marketing-pages/components/Blog/types.js';
+import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js';
+import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js';
+import { Box } from '@redocly/marketing-pages/ui/Box.js';
+
+import { RecentPosts } from '../@theme/components/Blog/RecentPosts';
+
+import ctaEclipse from './images/cta-eclipse.svg';
+
+export const frontmatter = {
+ title: 'Open-source, agent-friendly SDKs and tooling from OpenAPI description',
+ description:
+ 'Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents.',
+ seo: {
+ title: 'Open-source, agent-friendly SDKs and tooling from OpenAPI description',
+ description:
+ 'Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents.',
+ },
+ author: 'roman-marshevskyi',
+ publishedDate: '2026-08-27',
+ categories: ['redocly:redocly-cli', 'api-descriptions:openapi', 'api-lifecycle:sdks'],
+};
+
+export default function AgentFriendlySdksPost() {
+ const { usePageProps } = useThemeHooks();
+ const pageProps = usePageProps();
+
+ const { publishedDate, author, categories, title, slug } = (pageProps.metadata ?? {}) as Post & {
+ slug?: string;
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ One OpenAPI description, one command, and every consumer of your API gets a typed,
+ dependency-free artifact that regenerates instead of drifting.
+
+
+
+
+
+ As agents become part of engineering teams, more of your API calls are written by one.
+ Agents hallucinate endpoints, invent response fields, and hand-write API code you then
+ review line by line. Generated code is the cheapest, safest code an agent can ship, so
+ we built a generator that treats the agent as a first-class user.
+
+
+
+ Meet generate-client: a new command in the{' '}
+ Redocly CLI, powered by a new
+ package,{' '}
+
+ @redocly/client-generator
+
+ , that turns one OpenAPI description into typed SDKs in{' '}
+ TypeScript, Python, Go, and PHP, plus validation schemas, TanStack
+ Query and SWR hooks, test mocks, a ready-to-run command-line interface,
+ and reference docs for all of it. Both the command and the package are open source
+ (MIT), and everything they generate is yours outright.
+
+
+
+ The SDKs are fully featured with zero runtime dependencies: auth,
+ retries, middleware, pagination iterators, typed Server-Sent Events, query-string
+ serialization, and multipart uploads, all built on web-standard fetch,{' '}
+ AbortController, and URLSearchParams, emitted as code that
+ imports nothing. The API code your agent used to hallucinate becomes one deterministic
+ command, and the compiler becomes its fact-checker: operation ids, parameters, and
+ response fields are literal types, so a wrong call fails tsc with the exact
+ operation named.
+
+
+
Up and running in three steps
+
+
+
+ 1
+
+ Start from your API description
+
+ The one you already have: OpenAPI 3.0, 3.1, 3.2, or Swagger 2.0.
+
+
+
+ One description. One command. Every time your API changes.
+
+
The features you'd otherwise hand-write
+
+
+ Types are a third of the problem. The behavior is what teams hand-write around generated
+ types, and where drift starts. The generated client includes it:
+
+
+
+
+
+ Auth from your securitySchemes
+
+ : bearer, basic, and API keys in header, query, or cookie, each sent only where an
+ operation's security requires it. Credentials can be async token
+ providers, resolved on every request, so refresh flows need no extra code. Every
+ client instance carries its own.
+
+
+ Pagination: declare your pagination convention once (cursor, offset,
+ page, or Link header) and the iterators appear on the operation itself:{' '}
+ listOrders.pages(), listOrders.items(), typed, abortable,
+ with duplicate-cursor loop detection. Delete your pagination loops.
+
+
+ Opt-in, abort-aware retries: exponential backoff, jitter,{' '}
+ Retry-After, idempotent-only by default, and a custom{' '}
+ retryOn predicate.
+
+
+ Typed Server-Sent Events: an operation whose 2xx is{' '}
+ text/event-stream becomes a typed async iterator with automatic
+ reconnection, payloads typed from OpenAPI 3.2's itemSchema.
+
+
+ Composable middleware: onRequest,{' '}
+ onResponse, and onError, with operation ids, paths, and tags
+ visible to it as literal types.
+
+
+ The fiddly details, handled: query parameters serialized exactly as
+ the description declares, file uploads from a plain typed object, per-request
+ timeouts, and idempotency keys that make retries safe.
+
+
+ Two error models: exceptions by default, or a typed{' '}
+ {'{ data, error }'} result if you prefer returns over throws.
+
+
+
+
+ And it's strict on your behalf: a call with an argument the operation doesn't declare
+ fails before the request leaves the process, with an error that names the operation and
+ says where the argument belongs.
+
+
+
+ It reads OpenAPI 3.0, 3.1, and 3.2, plus Swagger 2.0{' '}
+ (normalized to 3.x before generation).
+
+
+
Skills first
+
+
+ Every part of this tool assumes an agent will operate it, and each of those decisions
+ helps the humans just as much:
+
+
+
+
+ The design ships as agent skills. Every generator carries its own
+ design document, and ejecting a generator drops it into your repo as a skill (
+ {'.claude/skills/-generator/'}) beside the authoring guide. An
+ agent asked to change generated output loads the rules first and edits the generator,
+ not the output.
+
+
+ A discoverable surface instead of prose. The generated CLI answers{' '}
+ --help with its commands and {'schema '} with one
+ operation's whole contract as JSON: method, path, parameters with types, request and
+ response schemas. An agent learns a real API in two commands.
+
+
+ Feedback an agent can act on. Strict types plus runtime
+ unknown-argument errors name the operation and say where the argument belongs.
+
+
+ Deterministic ground truth. The generated mocks are seeded and
+ offline, so tests an agent writes reproduce exactly, with no live API in the loop
+ teaching it wrong lessons.
+
+
+ Regeneration over hand-editing. The client is machine-owned and
+ rebuilt from the description; the generator is human-owned and ejectable. That split
+ tells an agent exactly which file it is allowed to change.
+
+
+
+
+ The instruction we ship our own agents is one paragraph:{' '}
+
+ never hand-write HTTP code for our APIs; regenerate the client and import the
+ functions, and a wrong call fails the build.
+
+
+
+
One description, every consumer
+
+
+ The vocabulary is simple: you select generators in one list, each
+ generator emits an artifact, and each artifact serves a different
+ consumer of your API. The SDK is one kind of artifact; here is the whole list, produced
+ from one parse of your description in one command:
+
+
+
+
+
+
+
Generator
+
Artifact
+
Consumer
+
+
+
+
+
+ typescript (default), python, go,{' '}
+ php
+
+
the full typed client, in that language
+
calling your API from any stack
+
+
+
+ zod
+
+
Zod schemas + validation middleware
+
runtime contract checks
+
+
+
+ tanstack-query, swr
+
+
query and mutation factories, hooks
+
React, Vue, Svelte, Solid data fetching
+
+
+
+ mock
+
+
MSW v2 handlers + typed data factories
+
tests and demos, offline and deterministic
+
+
+
+ transformers
+
+
+ Date converters
+
+
+ ISO strings → Date, paired with --date-type Date
+
+
+
+
+ cli
+
+
a bin-ready command-line interface
+
scripts, CI, agents
+
+
+
your own
+
anything
+
the long tail
+
+
+
+
+
+
+ Every language SDK carries the same behavior, each as a single self-contained file:{' '}
+ httpx for Python, the standard library for Go, the curl extension for PHP.
+ And names resolve once: listOrders is the operation in the description, the
+ function in every SDK, and the CLI command, so one identifier greps across your whole
+ stack.
+
+
+
+ Docs are one flag: add --docs and every selected generator writes a
+ reference page beside its output. The docs regenerate with the code, so they cannot
+ drift from it.
+
+
+
And if you disagree with a built-in, take it
+
+
+ When a tool gets something wrong for you, the traditional move is to fork it, and a fork
+ is a life sentence: you maintain the whole project from that day on, and upstream fixes
+ stop reaching you. Eject gives you the ownership without the fork:
+
+ That copies the built-in generator into your repository as{' '}
+ TypeScript source you own: a folder with one readable file per stage
+ (naming, types, models, operations, pagination, client). It wires your config to it,
+ and, unmodified, it produces byte-identical output. We verify that byte-identity in our
+ test suite. Later versions merge into your copy file by file with --update.
+ The generator's design document arrives with it as an{' '}
+ agent skill in your repo, and the skill is yours to manage: edit it to
+ state your house rules (naming, headers, error style, whatever the built-in got wrong
+ for you), and your AI agent reads the skill first and changes the ejected generator to
+ match. You maintain a short design document; the agent maintains the code to it.
+
+
+
Yours to shape
+
+
+
+ Call style: grouped inputs by default; --args-style flat{' '}
+ merges them into one object when an operation's inputs can't collide.
+
+
+ Output layout: one single file (default), or{' '}
+ split with schema types in a sibling module.
+
+
+ Runtime placement: inlined into the client by default for a truly
+ single-file artifact, or --runtime module to write the runtime as real,
+ readable files beside it, shared between clients.
+
+
+ No build step, if you want none: with --import-ext ts,
+ the generated client, the zod module, and the CLI run as they are under plain Node
+ 22.18+, which strips the types itself.
+
+
+ Configuration: CLI flags or a client block in{' '}
+ redocly.yaml, with per-API overrides for monorepos that generate several
+ clients from one config.
+
+
+
+
Proven on ourselves first
+
+
+ We didn't design this in the abstract: Redocly's own platform runs on this generator:
+ four internal APIs, hundreds of operations, an in-house codegen deleted in the process,
+ and much of the migration executed by an AI agent working against the generated client.
+ That migration found real bugs our tests had missed, and it's the subject of the next
+ post.
+
+
+
+ One caveat, stated plainly: the command is still experimental, flags and output may
+ change, so pin your CLI version. The code it generates is strict-TypeScript clean,
+ exhaustively tested, and already carrying Redocly's production traffic.
+
+
+
+
+
+
+
+
+
+ Try it on your own API
+
+ One command, no account, runs entirely on your machine.
+
+
+
+
+
@@ -123,10 +132,11 @@ export default function AgentFriendlySdksPost() {
No account, no config required. Flags or a redocly.yaml{' '}
client block, your choice.
-
+
@@ -137,28 +147,18 @@ export default function AgentFriendlySdksPost() {
Every operation is a typed function; every name comes from the description.
-
+
Then import a function and call your API. The whole client is in the file you just
generated.
-
- Command reference
-
-
- Write a custom generator
-
+ Command reference
+ Write a custom generator
Runnable examples
@@ -687,37 +686,9 @@ const BlogMediaBox = styled.div`
const Lead = styled.p`
font-size: 19px;
- line-height: 1.6;
-`;
-
-// The theme's Markdown wrapper styles 'pre' (background, text color, padding),
-// so token colors here are picked for contrast on its light code-block background.
-const Pre = styled.pre`
- border-radius: 8px;
- font-size: 13.5px;
- tab-size: 2;
- white-space: pre;
-`;
-
-const TokC = styled.span`
- color: #59636e;
-`;
-
-const TokK = styled.span`
- color: #cf222e;
-`;
-
-const TokS = styled.span`
- color: #116329;
-`;
-
-const TokF = styled.span`
- color: #8250df;
-`;
-
-const TokFlag = styled.span`
- color: #953800;
- font-weight: 600;
+ line-height: 1.65;
+ color: var(--color-text-dimmed);
+ margin-bottom: 2em;
`;
const Steps = styled.div`
@@ -772,11 +743,13 @@ const StepHint = styled.div`
margin: -4px 0 8px;
`;
-const StepsTagline = styled.p`
+const StepsTagline = styled(TextGradient)`
+ display: block;
font-weight: 700;
- font-size: 17px;
+ font-size: 22px;
+ line-height: 1.4;
text-align: center;
- margin: 22px 0 0;
+ margin: 48px 0 16px;
`;
const TableScroll = styled.div`
diff --git a/blog/metadata/blog-metadata.yaml b/blog/metadata/blog-metadata.yaml
index aaadea9f3..d4713625a 100644
--- a/blog/metadata/blog-metadata.yaml
+++ b/blog/metadata/blog-metadata.yaml
@@ -78,12 +78,6 @@ categories:
- id: dependency-maps
label: Dependency maps
- - id: api-descriptions
- label: API descriptions
- subcategories:
- - id: openapi
- label: OpenAPI
-
- id: api-documentation
label: API documentation
subcategories:
From 2444a4d4b7663f123507faea331dcd0b6e9b61cd Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Mon, 31 Aug 2026 11:55:25 +0300
Subject: [PATCH 05/14] Update blog/agent-friendly-sdks.page.tsx
Co-authored-by: Illia Adamchuk
---
blog/agent-friendly-sdks.page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/blog/agent-friendly-sdks.page.tsx b/blog/agent-friendly-sdks.page.tsx
index 1615ee80d..fe6031520 100644
--- a/blog/agent-friendly-sdks.page.tsx
+++ b/blog/agent-friendly-sdks.page.tsx
@@ -447,7 +447,7 @@ export default function AgentFriendlySdksPost() {
From 3d80b85464a474c373790d9548add074ce3e6b61 Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Mon, 31 Aug 2026 12:55:45 +0300
Subject: [PATCH 06/14] docs(blog): select frontmatter loader by route type
instead of path regex
Realm's markdown plugin stamps its routes with metadata.type === 'markdown',
so branch on that rather than matching the .page.tsx filename shape. Also
skip any post whose frontmatter cannot be loaded.
---
@theme/utils/blog-post.js | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/@theme/utils/blog-post.js b/@theme/utils/blog-post.js
index 563b97973..a5a23bac3 100644
--- a/@theme/utils/blog-post.js
+++ b/@theme/utils/blog-post.js
@@ -10,17 +10,19 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
const metadata = await transformMetadata(metadataContentRecord.data, context.fs.cwd, outdir);
for (const route of postRoutes) {
- // React blog posts export `frontmatter`; markdown posts use YAML frontmatter
- const isReactPage = /\.page\.tsx?$/.test(route.fsPath);
+ // Markdown routes are stamped with metadata.type === 'markdown' by the Realm
+ // markdown plugin; other blog post routes are React pages, which export
+ // `frontmatter` instead of using YAML frontmatter.
+ const isMarkdownPost = route.metadata?.type === 'markdown';
const { data } = await context.cache.load(
route.fsPath,
- isReactPage ? 'react-frontmatter' : 'markdown-frontmatter',
+ isMarkdownPost ? 'markdown-frontmatter' : 'react-frontmatter',
);
- const frontmatter = isReactPage ? data : data?.frontmatter;
+ const frontmatter = isMarkdownPost ? data?.frontmatter : data;
if (
- (isReactPage && !frontmatter) ||
- frontmatter?.ignore === true ||
+ !frontmatter ||
+ frontmatter.ignore === true ||
(await context.isPathIgnored(route.fsPath))
) {
continue;
From beb7fc02936fe763bacd9a61a254c00277845437 Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Mon, 31 Aug 2026 14:40:38 +0300
Subject: [PATCH 07/14] docs(blog): convert client generator post to markdown
and slim the PR
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Rewrite the post as a standard markdown blog post; the diagram is an
SVG image in the body
- Revert all theme changes (BlogPost template, plugin shared data,
frontmatter loader) — the recent-posts fix moves to a separate PR
- Address review feedback: complete the openapi.yaml snippet so the
TypeScript example compiles against it, explain agent skills with the
eject file layout, style the tagline as a quote, reword the caveat,
and refine the hero diagram (bigger subtitle, no window dots, thinner
accent-colored arrows)
---
@theme/components/Blog/RecentPosts.tsx | 52 --
@theme/plugin.js | 32 +-
@theme/templates/BlogPost.tsx | 64 +-
@theme/utils/blog-post.js | 36 +-
blog/agent-friendly-sdks.md | 198 ++++++
blog/agent-friendly-sdks.page.tsx | 922 -------------------------
blog/images/agent-friendly-sdks.svg | 62 ++
blog/images/cta-eclipse.svg | 16 -
8 files changed, 284 insertions(+), 1098 deletions(-)
delete mode 100644 @theme/components/Blog/RecentPosts.tsx
create mode 100644 blog/agent-friendly-sdks.md
delete mode 100644 blog/agent-friendly-sdks.page.tsx
create mode 100644 blog/images/agent-friendly-sdks.svg
delete mode 100644 blog/images/cta-eclipse.svg
diff --git a/@theme/components/Blog/RecentPosts.tsx b/@theme/components/Blog/RecentPosts.tsx
deleted file mode 100644
index fa1c99e12..000000000
--- a/@theme/components/Blog/RecentPosts.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import * as React from 'react';
-import styled from 'styled-components';
-
-import { useThemeHooks } from '@redocly/theme/core/hooks';
-
-import { ArticleCard } from '@redocly/marketing-pages/components/Blog/RecentPosts.js';
-import { H2Title } from '@redocly/marketing-pages/components/TypographyElements/TypographyElements.js';
-
-type RecentPost = { slug: string; title: string; description?: string };
-
-// Same layout as marketing-pages RecentPosts, but reads the deeper 'blog-recent-posts'
-// shared data and filters out the post it renders on, so a post never lists itself.
-export function RecentPosts({ currentSlug }: { currentSlug?: string }) {
- // @ts-ignore
- const { usePageSharedData } = useThemeHooks();
- const recentPosts = usePageSharedData('blog-recent-posts') ?? [];
-
- const posts = recentPosts.filter((post) => post.slug !== currentSlug).slice(0, 3);
-
- if (posts.length === 0) {
- return null;
- }
-
- return (
- <>
-
- Latest from our blog
-
-
- {posts.map((post) => (
-
- ))}
-
- >
- );
-}
-
-const RecentPostsGrid = styled.div`
- display: grid;
- grid-template-columns: 1fr;
- grid-gap: 5rem;
- justify-items: center;
-
- @media screen and (min-width: 900px) {
- grid-template-columns: 1fr 1fr 1fr;
- }
-`;
diff --git a/@theme/plugin.js b/@theme/plugin.js
index 4180912b0..5f3484125 100644
--- a/@theme/plugin.js
+++ b/@theme/plugin.js
@@ -7,7 +7,6 @@ const ABOUT_SLUG = '/about/';
const BLOG_METADATA_PATH = 'blog/metadata/blog-metadata.yaml';
const LATEST_POSTS_SHARED_DATA_ID = 'blog-latest-posts';
-const RECENT_POSTS_SHARED_DATA_ID = 'blog-recent-posts';
const ALL_POSTS_SHARED_DATA_ID = 'blog-posts';
function __dirname(url) {
@@ -27,11 +26,11 @@ export default function themePlugin() {
// Register preview route for the editor iframe
const previewTemplateId = actions.createTemplate(
'preview-template',
- fromCurrentDir(import.meta.url, './preview.route.tsx'),
+ fromCurrentDir(import.meta.url, './preview.route.tsx')
);
const blogTemplateId = actions.createTemplate(
- 'blog-template',
- fromCurrentDir(import.meta.url, './blog.page.tsx'),
+ 'blog-template',
+ fromCurrentDir(import.meta.url, './blog.page.tsx')
);
actions.addRoute({
excludeFromSidebar: true,
@@ -59,7 +58,7 @@ export default function themePlugin() {
templateId: blogTemplateId,
hasClientRoutes: true,
});
-
+
const metadataContentRecord = await context.cache.load(BLOG_METADATA_PATH, 'yaml');
const categories = metadataContentRecord.data.categories || [];
@@ -85,11 +84,10 @@ export default function themePlugin() {
// Existing blog data processing
const postRoutes = actions
.getAllRoutes()
- .filter(
- (route) =>
- route.slug.startsWith(BLOG_SLUG) &&
- route.slug !== BLOG_SLUG &&
- !route.slug.startsWith('/blog/category/'),
+ .filter((route) =>
+ route.slug.startsWith(BLOG_SLUG) &&
+ route.slug !== BLOG_SLUG &&
+ !route.slug.startsWith('/blog/category/')
);
const categoryRoutes = actions
@@ -100,14 +98,8 @@ export default function themePlugin() {
const latestPosts = postsData.posts.slice(0, 3);
- // Top 4 posts, so a post page can exclude itself and still show 3 recent posts
- const recentPosts = postsData.posts
- .slice(0, 4)
- .map(({ slug, title, description }) => ({ slug, title, description }));
-
// Create shared data for blog pages
await actions.createSharedData(LATEST_POSTS_SHARED_DATA_ID, latestPosts);
- await actions.createSharedData(RECENT_POSTS_SHARED_DATA_ID, recentPosts);
await actions.createSharedData(ALL_POSTS_SHARED_DATA_ID, postsData);
// Add latest posts shared data to all blog posts and update metadata
@@ -118,17 +110,11 @@ export default function themePlugin() {
LATEST_POSTS_SHARED_DATA_ID,
);
- actions.addRouteSharedData(
- post.slug,
- RECENT_POSTS_SHARED_DATA_ID,
- RECENT_POSTS_SHARED_DATA_ID,
- );
-
const postRoute = actions.getRouteBySlug(post.slug);
postRoute.metadata = { ...postRoute.metadata, ...post };
}
-
+
// Add all posts shared data to category routes
for (const categoryRoute of categoryRoutes) {
actions.addRouteSharedData(
diff --git a/@theme/templates/BlogPost.tsx b/@theme/templates/BlogPost.tsx
index 5e4db51f6..cde0630f7 100644
--- a/@theme/templates/BlogPost.tsx
+++ b/@theme/templates/BlogPost.tsx
@@ -1,63 +1,3 @@
-import React from 'react';
-import styled from 'styled-components';
+import Page from '@redocly/marketing-pages/templates/BlogPost.js';
-import type { Post } from '@redocly/marketing-pages/components/Blog/types.js';
-
-import { useThemeHooks } from '@redocly/theme/core/hooks';
-import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
-import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js';
-import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js';
-import { Box } from '@redocly/marketing-pages/ui/Box.js';
-
-import { RecentPosts } from '../components/Blog/RecentPosts';
-
-// Local version of @redocly/marketing-pages/templates/BlogPost.js — the only
-// difference is the RecentPosts section, which excludes the currently open post.
-export default function BlogPost(props: { children?: React.ReactNode }) {
- const { usePageProps } = useThemeHooks();
- const pageProps = usePageProps();
-
- const { publishedDate, author, categories, title, image, slug } = pageProps.metadata as Post & {
- slug?: string;
- };
-
- return (
-
-
-
-
- {props.children}
-
-
-
-
-
-
-
-
- );
-}
-
-const BlogMediaBox = styled.div`
- margin-left: auto;
- margin-right: auto;
- max-width: calc(90vw);
-
- @media screen and (min-width: 900px) {
- max-width: 800px;
- }
-`;
-
-const PageWrapper = styled.div`
- position: relative;
- overflow: hidden;
-`;
+export default Page;
\ No newline at end of file
diff --git a/@theme/utils/blog-post.js b/@theme/utils/blog-post.js
index a5a23bac3..8ed216162 100644
--- a/@theme/utils/blog-post.js
+++ b/@theme/utils/blog-post.js
@@ -10,21 +10,11 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
const metadata = await transformMetadata(metadataContentRecord.data, context.fs.cwd, outdir);
for (const route of postRoutes) {
- // Markdown routes are stamped with metadata.type === 'markdown' by the Realm
- // markdown plugin; other blog post routes are React pages, which export
- // `frontmatter` instead of using YAML frontmatter.
- const isMarkdownPost = route.metadata?.type === 'markdown';
- const { data } = await context.cache.load(
- route.fsPath,
- isMarkdownPost ? 'markdown-frontmatter' : 'react-frontmatter',
- );
- const frontmatter = isMarkdownPost ? data?.frontmatter : data;
+ const {
+ data: { content, frontmatter },
+ } = await context.cache.load(route.fsPath, 'markdown-frontmatter');
- if (
- !frontmatter ||
- frontmatter.ignore === true ||
- (await context.isPathIgnored(route.fsPath))
- ) {
+ if (frontmatter?.ignore === true || (await context.isPathIgnored(route.fsPath))) {
continue;
}
@@ -36,15 +26,15 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
.map((categoryId) => {
const categoryData = metadata.categories.get(categoryId);
if (!categoryData) return null;
-
+
if (categoryData.category && categoryData.subcategory) {
- return categoryData;
+ return categoryData;
} else {
- return {
+ return {
category: {
- id: categoryData.id,
- label: categoryData.label,
- },
+ id: categoryData.id,
+ label: categoryData.label
+ }
};
}
})
@@ -82,12 +72,12 @@ async function transformMetadata(metadata, cwd, outdir) {
categories.set(fullId, {
category: {
id: category.id,
- label: category.label,
+ label: category.label
},
subcategory: {
id: subcategory.id,
- label: subcategory.label,
- },
+ label: subcategory.label
+ }
});
}
}
diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md
new file mode 100644
index 000000000..af9606d61
--- /dev/null
+++ b/blog/agent-friendly-sdks.md
@@ -0,0 +1,198 @@
+---
+template: ../@theme/templates/BlogPost
+title: Open-source, agent-friendly SDKs and tooling from OpenAPI description
+description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents."
+seo:
+ title: Open-source, agent-friendly SDKs and tooling from OpenAPI description
+ description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents."
+author: roman-marshevskyi
+publishedDate: '2026-08-27'
+categories:
+ - redocly:redocly-cli
+ - api-specifications:openapi
+ - api-lifecycle:sdks
+---
+
+# Open-source, agent-friendly SDKs and tooling from OpenAPI description
+
+As agents become part of engineering teams, more of your API calls are written by one.
+Agents hallucinate endpoints, invent response fields, and hand-write API code you then review line by line.
+Generated code is the cheapest, safest code an agent can ship, so we built a generator that treats the agent as a first-class user.
+
+
+
+Meet `generate-client`: a new command in the [Redocly CLI](https://github.com/Redocly/redocly-cli), powered by a new package, [`@redocly/client-generator`](https://github.com/Redocly/redocly-cli/tree/main/packages/client-generator), that turns one OpenAPI description into typed SDKs in **TypeScript, Python, Go, and PHP**, plus validation schemas, TanStack Query and SWR hooks, test mocks, a ready-to-run **command-line interface**, and reference docs for all of it.
+Both the command and the package are open source (MIT), and everything they generate is yours outright.
+
+The SDKs are fully featured with **zero runtime dependencies**: auth, retries, middleware, pagination iterators, typed Server-Sent Events, query-string serialization, and multipart uploads, all built on web-standard `fetch`, `AbortController`, and `URLSearchParams`, emitted as code that imports nothing.
+The API code your agent used to hallucinate becomes one deterministic command, and the compiler becomes its fact-checker: operation ids, parameters, and response fields are literal types, so a wrong call fails `tsc` with the exact operation named.
+
+## Up and running in three steps
+
+### 1. Start from your API description
+
+The one you already have: OpenAPI 3.0, 3.1, 3.2, or Swagger 2.0.
+
+```yaml
+paths:
+ /menu-items:
+ get:
+ operationId: listMenuItems
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ /orders/{orderId}:
+ get:
+ operationId: getOrderById
+ security:
+ - BearerAuth: []
+ parameters:
+ - name: orderId
+ in: path
+ required: true
+ schema:
+ type: string
+components:
+ securitySchemes:
+ BearerAuth:
+ type: http
+ scheme: bearer
+```
+
+### 2. Run one command
+
+No account, no config file — every option has a default.
+Customize with flags or a `redocly.yaml` `client` block when you need to.
+
+```bash
+npx @redocly/cli@latest generate-client openapi.yaml --output src/client.ts
+```
+
+### 3. Call your API
+
+Every operation is a typed function; every name comes from the description.
+
+```typescript
+import { configure, listMenuItems, getOrderById } from './client.js';
+
+configure({ auth: { bearer: token } }); // sent only where an operation requires it
+
+const menu = await listMenuItems({ query: { limit: 10 } });
+const order = await getOrderById({ path: { orderId: 'ord_01khr…' } });
+```
+
+That's the whole client.
+
+> One description. One command. Every time your API changes.
+
+## The features you'd otherwise hand-write
+
+Types are a third of the problem.
+The behavior is what teams hand-write around generated types, and where drift starts.
+The generated client includes it:
+
+- **Auth from your `securitySchemes`**: bearer, basic, and API keys in header, query, or cookie, each sent only where an operation's `security` requires it. Credentials can be async token providers, resolved on every request, so refresh flows need no extra code. Every client instance carries its own.
+- **Pagination**: declare your pagination convention once (cursor, offset, page, or `Link` header) and the iterators appear on the operation itself: `listOrders.pages()`, `listOrders.items()`, typed, abortable, with duplicate-cursor loop detection. Delete your pagination loops.
+- **Opt-in, abort-aware retries**: exponential backoff, jitter, `Retry-After`, idempotent-only by default, and a custom `retryOn` predicate.
+- **Typed Server-Sent Events**: an operation whose `2xx` is `text/event-stream` becomes a typed async iterator with automatic reconnection, payloads typed from OpenAPI 3.2's `itemSchema`.
+- **Composable middleware**: `onRequest`, `onResponse`, and `onError`, with operation ids, paths, and tags visible to it as literal types.
+- **The fiddly details, handled**: query parameters serialized exactly as the description declares, file uploads from a plain typed object, per-request timeouts, and idempotency keys that make retries safe.
+- **Two error models**: exceptions by default, or a typed `{ data, error }` result if you prefer returns over throws.
+
+And it's strict on your behalf: a call with an argument the operation doesn't declare fails before the request leaves the process, with an error that names the operation and says where the argument belongs.
+
+It reads OpenAPI **3.0, 3.1, and 3.2**, plus **Swagger 2.0** (normalized to 3.x before generation).
+
+## Skills first
+
+Every part of this tool assumes an agent will operate it, and each of those decisions helps the humans just as much:
+
+- **The design ships as agent skills.** A skill is a short instruction file that AI agents (such as Claude Code) load before touching related code: it states what a piece of code is for, the rules it must follow, and how to change it safely. Every generator carries its own design document as a skill, and ejecting a generator drops both into your repo beside the generator source:
+
+ ```text
+ redocly eject-generator zod
+
+ generators/zod/… # the generator source, now yours
+ generators/AGENTS.md # pointer that leads agents to the skills
+ .claude/skills/client-generators/SKILL.md # the shared authoring guide
+ .claude/skills/zod-generator/SKILL.md # why this generator is built the way it is
+ ```
+
+ An agent asked to change generated output loads the rules first and edits the generator, not the output.
+- **A discoverable surface instead of prose.** The generated CLI answers `--help` with its commands and `schema ` with one operation's whole contract as JSON: method, path, parameters with types, request and response schemas. An agent learns a real API in two commands.
+- **Feedback an agent can act on.** Strict types plus runtime unknown-argument errors name the operation and say where the argument belongs.
+- **Deterministic ground truth.** The generated mocks are seeded and offline, so tests an agent writes reproduce exactly, with no live API in the loop teaching it wrong lessons.
+- **Regeneration over hand-editing.** The client is machine-owned and rebuilt from the description; the generator is human-owned and ejectable. That split tells an agent exactly which file it is allowed to change.
+
+The whole instruction your agents need is one sentence: _never hand-write HTTP code for our APIs - regenerate the client and import the functions, and a wrong call fails the build._
+
+## One description, every consumer
+
+The vocabulary is simple: you select **generators** in one list, each generator emits an **artifact**, and each artifact serves a different consumer of your API.
+The SDK is one kind of artifact; here is the whole list, produced from one parse of your description in one command:
+
+| Generator | Artifact | Consumer |
+| --- | --- | --- |
+| `typescript` (default), `python`, `go`, `php` | the full typed client, in that language | calling your API from any stack |
+| `zod` | Zod schemas + validation middleware | runtime contract checks |
+| `tanstack-query`, `swr` | query and mutation factories, hooks | React, Vue, Svelte, Solid data fetching |
+| `mock` | MSW v2 handlers + typed data factories | tests and demos, offline and deterministic |
+| `transformers` | `Date` converters | ISO strings → `Date`, paired with `--date-type Date` |
+| `cli` | a bin-ready command-line interface | scripts, CI, agents |
+| your own | anything | the long tail |
+
+Every language SDK carries the same behavior, each as a single self-contained file: `httpx` for Python, the standard library for Go, the curl extension for PHP.
+And names resolve once: `listOrders` is the operation in the description, the function in every SDK, and the CLI command, so one identifier greps across your whole stack.
+
+Docs are one flag: add `--docs` and every selected generator writes a reference page beside its output.
+The docs regenerate with the code, so they cannot drift from it.
+
+## And if you disagree with a built-in, take it
+
+When a tool gets something wrong for you, the traditional move is to fork it, and a fork is a life sentence: you maintain the whole project from that day on, and upstream fixes stop reaching you.
+Eject gives you the ownership without the fork:
+
+```bash
+npx @redocly/cli@latest eject-generator python
+```
+
+That copies the built-in generator into your repository as **TypeScript source you own**: a folder with one readable file per stage (naming, types, models, operations, pagination, client).
+It wires your config to it, and, unmodified, it produces byte-identical output.
+We verify that byte-identity in our test suite.
+Later versions merge into your copy file by file with `--update`.
+The generator's design document arrives with it as an **agent skill in your repo**, and the skill is yours to manage: edit it to state your house rules (naming, headers, error style, whatever the built-in got wrong for you), and your AI agent reads the skill first and changes the ejected generator to match.
+You maintain a short design document; the agent maintains the code to it.
+
+## Yours to shape
+
+- **Call style**: grouped inputs by default; `--args-style flat` merges them into one object when an operation's inputs can't collide.
+- **Output layout**: one `single` file (default), or `split` with schema types in a sibling module.
+- **Runtime placement**: inlined into the client by default for a truly single-file artifact, or `--runtime module` to write the runtime as real, readable files beside it, shared between clients.
+- **No build step, if you want none**: with `--import-ext ts`, the generated client, the zod module, and the CLI run as they are under plain Node 22.18+, which strips the types itself.
+- **Configuration**: CLI flags or a `client` block in `redocly.yaml`, with per-API overrides for monorepos that generate several clients from one config.
+
+## Proven on ourselves first
+
+We didn't design this in the abstract: Redocly's own platform runs on this generator: four internal APIs, hundreds of operations, an in-house codegen deleted in the process, and much of the migration executed by an AI agent working against the generated client.
+That migration found real bugs our tests had missed, and it's the subject of the next post.
+
+A quick caveat: the command is still experimental, so flags and output may change between releases — pin your CLI version.
+The code it generates is strict-TypeScript clean, exhaustively tested, and already carrying Redocly's production traffic.
+
+## Try it
+
+One command, no account, runs entirely on your machine:
+
+```bash
+npx @redocly/cli@latest generate-client openapi.yaml --output src/client.ts
+```
+
+Then import a function and call your API.
+The whole client is in the file you just generated.
+
+- [Command reference](/docs/cli/commands/generate-client)
+- [Write a custom generator](/docs/cli/guides/customize-client-generation)
+- [Runnable examples](https://github.com/Redocly/redocly-cli/tree/main/tests/e2e/generate-client/examples)
+- [GitHub](https://github.com/Redocly/redocly-cli)
diff --git a/blog/agent-friendly-sdks.page.tsx b/blog/agent-friendly-sdks.page.tsx
deleted file mode 100644
index fe6031520..000000000
--- a/blog/agent-friendly-sdks.page.tsx
+++ /dev/null
@@ -1,922 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-
-import { useThemeHooks } from '@redocly/theme/core/hooks';
-import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
-import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock';
-
-import type { Post } from '@redocly/marketing-pages/components/Blog/types.js';
-import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js';
-import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js';
-import { TextGradient } from '@redocly/marketing-pages/components/TextGradient/TextGradient.js';
-import { Box } from '@redocly/marketing-pages/ui/Box.js';
-
-import { RecentPosts } from '../@theme/components/Blog/RecentPosts';
-
-import ctaEclipse from './images/cta-eclipse.svg';
-
-export const frontmatter = {
- title: 'Open-source, agent-friendly SDKs and tooling from OpenAPI description',
- description:
- 'Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents.',
- seo: {
- title: 'Open-source, agent-friendly SDKs and tooling from OpenAPI description',
- description:
- 'Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents.',
- },
- author: 'roman-marshevskyi',
- publishedDate: '2026-08-27',
- categories: ['redocly:redocly-cli', 'api-specifications:openapi', 'api-lifecycle:sdks'],
-};
-
-export default function AgentFriendlySdksPost() {
- const { usePageProps } = useThemeHooks();
- const pageProps = usePageProps();
-
- const { publishedDate, author, categories, title, slug } = (pageProps.metadata ?? {}) as Post & {
- slug?: string;
- };
-
- return (
-
-
-
-
-
-
-
-
-
- One OpenAPI description, one command, and every consumer of your API gets a typed,
- dependency-free artifact that regenerates instead of drifting.
-
-
-
-
-
- As agents become part of engineering teams, more of your API calls are written by one.
- Agents hallucinate endpoints, invent response fields, and hand-write API code you then
- review line by line. Generated code is the cheapest, safest code an agent can ship, so
- we built a generator that treats the agent as a first-class user.
-
-
-
- Meet generate-client: a new command in the{' '}
- Redocly CLI, powered by a new
- package,{' '}
-
- @redocly/client-generator
-
- , that turns one OpenAPI description into typed SDKs in{' '}
- TypeScript, Python, Go, and PHP, plus validation schemas, TanStack
- Query and SWR hooks, test mocks, a ready-to-run command-line interface,
- and reference docs for all of it. Both the command and the package are open source
- (MIT), and everything they generate is yours outright.
-
-
-
- The SDKs are fully featured with zero runtime dependencies: auth,
- retries, middleware, pagination iterators, typed Server-Sent Events, query-string
- serialization, and multipart uploads, all built on web-standard fetch,{' '}
- AbortController, and URLSearchParams, emitted as code that
- imports nothing. The API code your agent used to hallucinate becomes one deterministic
- command, and the compiler becomes its fact-checker: operation ids, parameters, and
- response fields are literal types, so a wrong call fails tsc with the exact
- operation named.
-
-
-
Up and running in three steps
-
-
-
- 1
-
- Start from your API description
-
- The one you already have: OpenAPI 3.0, 3.1, 3.2, or Swagger 2.0.
-
-
-
-
-
-
- 2
-
- Run one command
-
- No account, no config required. Flags or a redocly.yaml{' '}
- client block, your choice.
-
-
-
-
-
-
- 3
-
- Call your API
-
- Every operation is a typed function; every name comes from the description.
-
-
-
-
-
-
-
That's the whole client.
-
- One description. One command. Every time your API changes.
-
-
The features you'd otherwise hand-write
-
-
- Types are a third of the problem. The behavior is what teams hand-write around generated
- types, and where drift starts. The generated client includes it:
-
-
-
-
-
- Auth from your securitySchemes
-
- : bearer, basic, and API keys in header, query, or cookie, each sent only where an
- operation's security requires it. Credentials can be async token
- providers, resolved on every request, so refresh flows need no extra code. Every
- client instance carries its own.
-
-
- Pagination: declare your pagination convention once (cursor, offset,
- page, or Link header) and the iterators appear on the operation itself:{' '}
- listOrders.pages(), listOrders.items(), typed, abortable,
- with duplicate-cursor loop detection. Delete your pagination loops.
-
-
- Opt-in, abort-aware retries: exponential backoff, jitter,{' '}
- Retry-After, idempotent-only by default, and a custom{' '}
- retryOn predicate.
-
-
- Typed Server-Sent Events: an operation whose 2xx is{' '}
- text/event-stream becomes a typed async iterator with automatic
- reconnection, payloads typed from OpenAPI 3.2's itemSchema.
-
-
- Composable middleware: onRequest,{' '}
- onResponse, and onError, with operation ids, paths, and tags
- visible to it as literal types.
-
-
- The fiddly details, handled: query parameters serialized exactly as
- the description declares, file uploads from a plain typed object, per-request
- timeouts, and idempotency keys that make retries safe.
-
-
- Two error models: exceptions by default, or a typed{' '}
- {'{ data, error }'} result if you prefer returns over throws.
-
-
-
-
- And it's strict on your behalf: a call with an argument the operation doesn't declare
- fails before the request leaves the process, with an error that names the operation and
- says where the argument belongs.
-
-
-
- It reads OpenAPI 3.0, 3.1, and 3.2, plus Swagger 2.0{' '}
- (normalized to 3.x before generation).
-
-
-
Skills first
-
-
- Every part of this tool assumes an agent will operate it, and each of those decisions
- helps the humans just as much:
-
-
-
-
- The design ships as agent skills. Every generator carries its own
- design document, and ejecting a generator drops it into your repo as a skill (
- {'.claude/skills/-generator/'}) beside the authoring guide. An
- agent asked to change generated output loads the rules first and edits the generator,
- not the output.
-
-
- A discoverable surface instead of prose. The generated CLI answers{' '}
- --help with its commands and {'schema '} with one
- operation's whole contract as JSON: method, path, parameters with types, request and
- response schemas. An agent learns a real API in two commands.
-
-
- Feedback an agent can act on. Strict types plus runtime
- unknown-argument errors name the operation and say where the argument belongs.
-
-
- Deterministic ground truth. The generated mocks are seeded and
- offline, so tests an agent writes reproduce exactly, with no live API in the loop
- teaching it wrong lessons.
-
-
- Regeneration over hand-editing. The client is machine-owned and
- rebuilt from the description; the generator is human-owned and ejectable. That split
- tells an agent exactly which file it is allowed to change.
-
-
-
-
- The instruction we ship our own agents is one paragraph:{' '}
-
- never hand-write HTTP code for our APIs; regenerate the client and import the
- functions, and a wrong call fails the build.
-
-
-
-
One description, every consumer
-
-
- The vocabulary is simple: you select generators in one list, each
- generator emits an artifact, and each artifact serves a different
- consumer of your API. The SDK is one kind of artifact; here is the whole list, produced
- from one parse of your description in one command:
-
-
-
-
-
-
-
Generator
-
Artifact
-
Consumer
-
-
-
-
-
- typescript (default), python, go,{' '}
- php
-
-
the full typed client, in that language
-
calling your API from any stack
-
-
-
- zod
-
-
Zod schemas + validation middleware
-
runtime contract checks
-
-
-
- tanstack-query, swr
-
-
query and mutation factories, hooks
-
React, Vue, Svelte, Solid data fetching
-
-
-
- mock
-
-
MSW v2 handlers + typed data factories
-
tests and demos, offline and deterministic
-
-
-
- transformers
-
-
- Date converters
-
-
- ISO strings → Date, paired with --date-type Date
-
-
-
-
- cli
-
-
a bin-ready command-line interface
-
scripts, CI, agents
-
-
-
your own
-
anything
-
the long tail
-
-
-
-
-
-
- Every language SDK carries the same behavior, each as a single self-contained file:{' '}
- httpx for Python, the standard library for Go, the curl extension for PHP.
- And names resolve once: listOrders is the operation in the description, the
- function in every SDK, and the CLI command, so one identifier greps across your whole
- stack.
-
-
-
- Docs are one flag: add --docs and every selected generator writes a
- reference page beside its output. The docs regenerate with the code, so they cannot
- drift from it.
-
-
-
And if you disagree with a built-in, take it
-
-
- When a tool gets something wrong for you, the traditional move is to fork it, and a fork
- is a life sentence: you maintain the whole project from that day on, and upstream fixes
- stop reaching you. Eject gives you the ownership without the fork:
-
-
-
-
-
- That copies the built-in generator into your repository as{' '}
- TypeScript source you own: a folder with one readable file per stage
- (naming, types, models, operations, pagination, client). It wires your config to it,
- and, unmodified, it produces byte-identical output. We verify that byte-identity in our
- test suite. Later versions merge into your copy file by file with --update.
- The generator's design document arrives with it as an{' '}
- agent skill in your repo, and the skill is yours to manage: edit it to
- state your house rules (naming, headers, error style, whatever the built-in got wrong
- for you), and your AI agent reads the skill first and changes the ejected generator to
- match. You maintain a short design document; the agent maintains the code to it.
-
-
-
Yours to shape
-
-
-
- Call style: grouped inputs by default; --args-style flat{' '}
- merges them into one object when an operation's inputs can't collide.
-
-
- Output layout: one single file (default), or{' '}
- split with schema types in a sibling module.
-
-
- Runtime placement: inlined into the client by default for a truly
- single-file artifact, or --runtime module to write the runtime as real,
- readable files beside it, shared between clients.
-
-
- No build step, if you want none: with --import-ext ts,
- the generated client, the zod module, and the CLI run as they are under plain Node
- 22.18+, which strips the types itself.
-
-
- Configuration: CLI flags or a client block in{' '}
- redocly.yaml, with per-API overrides for monorepos that generate several
- clients from one config.
-
-
-
-
Proven on ourselves first
-
-
- We didn't design this in the abstract: Redocly's own platform runs on this generator:
- four internal APIs, hundreds of operations, an in-house codegen deleted in the process,
- and much of the migration executed by an AI agent working against the generated client.
- That migration found real bugs our tests had missed, and it's the subject of the next
- post.
-
-
-
- One caveat, stated plainly: the command is still experimental, flags and output may
- change, so pin your CLI version. The code it generates is strict-TypeScript clean,
- exhaustively tested, and already carrying Redocly's production traffic.
-
-
-
-
-
-
-
-
-
- Try it on your own API
-
- One command, no account, runs entirely on your machine.
-
-
-
-
-
- Then import a function and call your API. The whole client is in the file you just
- generated.
-
-
- Command reference
- Write a custom generator
-
- Runnable examples
-
- GitHub
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function HeroDiagram() {
- return (
-
- );
-}
-
-const HeroFigure = styled.figure`
- margin: 6px 0 36px;
-
- .frame {
- border: 1px solid #ededf2;
- border-radius: 12px;
- overflow: hidden;
- background: #ffffff;
- }
-
- svg {
- display: block;
- width: 100%;
- height: auto;
- }
-
- figcaption {
- font-size: 13px;
- color: #6e6f7a;
- margin-top: 10px;
- }
-`;
-
-const PageWrapper = styled.div`
- position: relative;
- overflow: hidden;
-`;
-
-const BlogMediaBox = styled.div`
- margin-left: auto;
- margin-right: auto;
- max-width: calc(90vw);
-
- @media screen and (min-width: 900px) {
- max-width: 800px;
- }
-`;
-
-const Lead = styled.p`
- font-size: 19px;
- line-height: 1.65;
- color: var(--color-text-dimmed);
- margin-bottom: 2em;
-`;
-
-const Steps = styled.div`
- display: flex;
- flex-direction: column;
- gap: 18px;
- margin: 26px 0 10px;
-`;
-
-const Step = styled.div`
- display: grid;
- grid-template-columns: 34px 1fr;
- gap: 14px;
-
- /* Let the code block shrink and scroll instead of widening the column */
- > div {
- min-width: 0;
- }
-
- pre {
- margin-bottom: 0;
- }
-
- @media (max-width: 560px) {
- grid-template-columns: 1fr;
- }
-`;
-
-const StepNumber = styled.div`
- width: 34px;
- height: 34px;
- border-radius: 999px;
- background: #e7f3ff;
- color: #2467f2;
- font-weight: 800;
- font-size: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-top: 2px;
-`;
-
-const StepLabel = styled.div`
- font-weight: 700;
- font-size: 16.5px;
- margin: 6px 0 8px;
-`;
-
-const StepHint = styled.div`
- font-size: 14px;
- color: var(--color-text-dimmed, #6e6f7a);
- margin: -4px 0 8px;
-`;
-
-const StepsTagline = styled(TextGradient)`
- display: block;
- font-weight: 700;
- font-size: 22px;
- line-height: 1.4;
- text-align: center;
- margin: 48px 0 16px;
-`;
-
-const TableScroll = styled.div`
- overflow-x: auto;
- margin: 0 0 1.3em;
- border: 1px solid #ededf2;
- border-radius: 8px;
-
- table {
- border-collapse: collapse;
- width: 100%;
- min-width: 560px; /* scroll horizontally on small screens instead of squashing columns */
- font-size: 14.5px;
- margin: 0;
- }
-
- th,
- td {
- text-align: left;
- padding: 11px 16px;
- border-bottom: 1px solid #ededf2;
- vertical-align: top;
- }
-
- thead th {
- background: #fbfbfc;
- font-weight: 600;
- white-space: nowrap;
- }
-
- tbody tr:last-child td {
- border-bottom: none;
- }
-`;
-
-// CTA card in the style of the /reviewer page: tonal card, split columns, eclipse glow.
-const CtaCard = styled.div`
- position: relative;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- margin-top: 64px;
- border-radius: 32px;
- background-color: var(--bg-color-tonal);
-
- > div:last-child {
- border-top: 1px solid var(--border-color-secondary);
- }
-
- @media screen and (min-width: 768px) {
- flex-direction: row;
-
- > div:last-child {
- border-left: 1px solid var(--border-color-secondary);
- border-top: none;
- }
- }
-`;
-
-/* The "Eclipse" glow from the reviewer page CTA: a blurred pink→violet ellipse
- anchored to the card's bottom-left, clipped by the card. */
-const CtaGlow = styled.div`
- position: absolute;
- top: 135px;
- left: -37px;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 564px;
- height: 369px;
- pointer-events: none;
-`;
-
-const CtaGlowInner = styled.div`
- position: relative;
- flex: none;
- width: 267px;
- height: 557px;
- transform: rotate(93.43deg) scaleY(0.99) skewX(-7.19deg);
-
- img {
- position: absolute;
- inset: -35.89% -75.04%;
- width: 250.08%;
- height: 171.78%;
- max-width: none;
- }
-`;
-
-const CtaTitleColumn = styled.div`
- position: relative;
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 32px 32px 0;
-
- @media screen and (min-width: 768px) {
- padding: 40px 32px 40px 40px;
- }
-`;
-
-const CtaTitle = styled.p`
- margin: 0;
- font-family: 'Red Hat Display';
- font-size: 32px;
- font-weight: 700;
- line-height: 40px;
- color: var(--text-color-primary);
-`;
-
-const CtaDescription = styled.p`
- margin: 0;
- font-family: 'Red Hat Display';
- font-size: 18px;
- font-weight: 500;
- line-height: 26px;
- color: var(--text-color-helper);
-`;
-
-const CtaActionColumn = styled.div`
- position: relative;
- display: flex;
- flex: 1.4;
- min-width: 0;
- flex-direction: column;
- gap: 20px;
- padding: 32px;
-
- @media screen and (min-width: 768px) {
- padding: 40px;
- }
-
- &&& pre {
- margin: 0;
- background-color: var(--bg-color);
- border: 1px solid var(--border-color-secondary);
- white-space: pre-wrap;
- word-break: break-word;
- }
-`;
-
-const CtaNote = styled.p`
- margin: 0;
- font-size: 14.5px;
- line-height: 1.55;
- color: var(--text-color-helper);
-`;
-
-const CtaLinks = styled.div`
- display: grid;
- grid-template-columns: repeat(2, minmax(0, max-content));
- justify-content: start;
- gap: 12px 40px;
- font-size: 14.5px;
-
- a {
- font-weight: 600;
- text-decoration: none;
-
- &:hover {
- text-decoration: underline;
- }
- }
-
- @media (max-width: 400px) {
- grid-template-columns: minmax(0, max-content);
- }
-`;
diff --git a/blog/images/agent-friendly-sdks.svg b/blog/images/agent-friendly-sdks.svg
new file mode 100644
index 000000000..eff9b66a3
--- /dev/null
+++ b/blog/images/agent-friendly-sdks.svg
@@ -0,0 +1,62 @@
+
diff --git a/blog/images/cta-eclipse.svg b/blog/images/cta-eclipse.svg
deleted file mode 100644
index bc7ce0c7c..000000000
--- a/blog/images/cta-eclipse.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
From 674d74843a49730121f8018651aa76d668a56fd9 Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Mon, 31 Aug 2026 14:41:43 +0300
Subject: [PATCH 08/14] docs(blog): revert markdownlint workflow change
The mcp-server markdownlint exclusion belongs in its own PR.
---
.github/workflows/docs-tests.yaml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml
index 7397c29cd..2e92dc4b0 100644
--- a/.github/workflows/docs-tests.yaml
+++ b/.github/workflows/docs-tests.yaml
@@ -27,4 +27,3 @@ jobs:
!docs/realm/.templates/*
!docs/realm/customization/add-color-mode.md
!docs/realm/customization/eject-components/eject-components-tutorial/index.md
- !docs/realm/customization/mcp-server/index.md
From c88718a23f836582f62a51614f410201c1394e3c Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Mon, 31 Aug 2026 16:11:12 +0300
Subject: [PATCH 09/14] docs(blog): bump publishedDate to 2026-08-31
---
blog/agent-friendly-sdks.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md
index af9606d61..b83a49866 100644
--- a/blog/agent-friendly-sdks.md
+++ b/blog/agent-friendly-sdks.md
@@ -6,7 +6,7 @@ seo:
title: Open-source, agent-friendly SDKs and tooling from OpenAPI description
description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents."
author: roman-marshevskyi
-publishedDate: '2026-08-27'
+publishedDate: '2026-08-31'
categories:
- redocly:redocly-cli
- api-specifications:openapi
From 8ef3d24238e2ef6955676017c4b8332ec6c7c399 Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Tue, 1 Sep 2026 13:38:04 +0300
Subject: [PATCH 10/14] docs(blog): bump publishedDate to 2026-09-01
---
blog/agent-friendly-sdks.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md
index b83a49866..a8a69f6b9 100644
--- a/blog/agent-friendly-sdks.md
+++ b/blog/agent-friendly-sdks.md
@@ -6,7 +6,7 @@ seo:
title: Open-source, agent-friendly SDKs and tooling from OpenAPI description
description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents."
author: roman-marshevskyi
-publishedDate: '2026-08-31'
+publishedDate: '2026-09-01'
categories:
- redocly:redocly-cli
- api-specifications:openapi
From 317969cb0686b29ece5a79453283e5b25fd4ed2c Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Tue, 1 Sep 2026 16:48:30 +0300
Subject: [PATCH 11/14] docs(blog): use client instance style in the example
and scope the zero-deps claim
- Show the exported client object (client.auth.bearer, client.listMenuItems)
so it is clear the auth is stored on the client the calls go through
- Zero runtime dependencies / fetch applies to the TypeScript client;
other languages use their own HTTP layer (httpx, stdlib, curl)
---
blog/agent-friendly-sdks.md | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md
index a8a69f6b9..05aafeb7c 100644
--- a/blog/agent-friendly-sdks.md
+++ b/blog/agent-friendly-sdks.md
@@ -24,7 +24,8 @@ Generated code is the cheapest, safest code an agent can ship, so we built a gen
Meet `generate-client`: a new command in the [Redocly CLI](https://github.com/Redocly/redocly-cli), powered by a new package, [`@redocly/client-generator`](https://github.com/Redocly/redocly-cli/tree/main/packages/client-generator), that turns one OpenAPI description into typed SDKs in **TypeScript, Python, Go, and PHP**, plus validation schemas, TanStack Query and SWR hooks, test mocks, a ready-to-run **command-line interface**, and reference docs for all of it.
Both the command and the package are open source (MIT), and everything they generate is yours outright.
-The SDKs are fully featured with **zero runtime dependencies**: auth, retries, middleware, pagination iterators, typed Server-Sent Events, query-string serialization, and multipart uploads, all built on web-standard `fetch`, `AbortController`, and `URLSearchParams`, emitted as code that imports nothing.
+The SDKs are fully featured: auth, retries, middleware, pagination iterators, typed Server-Sent Events, query-string serialization, and multipart uploads, with no dependencies beyond each language's own HTTP layer.
+The TypeScript client is built on web-standard `fetch`, `AbortController`, and `URLSearchParams` — **zero runtime dependencies**, emitted as code that imports nothing.
The API code your agent used to hallucinate becomes one deterministic command, and the compiler becomes its fact-checker: operation ids, parameters, and response fields are literal types, so a wrong call fails `tsc` with the exact operation named.
## Up and running in three steps
@@ -72,15 +73,15 @@ npx @redocly/cli@latest generate-client openapi.yaml --output src/client.ts
### 3. Call your API
-Every operation is a typed function; every name comes from the description.
+Every operation is a typed method on the generated client; every name comes from the description.
```typescript
-import { configure, listMenuItems, getOrderById } from './client.js';
+import { client } from './client.js';
-configure({ auth: { bearer: token } }); // sent only where an operation requires it
+client.auth.bearer(token); // stored on this client; sent only with operations whose security requires it
-const menu = await listMenuItems({ query: { limit: 10 } });
-const order = await getOrderById({ path: { orderId: 'ord_01khr…' } });
+const menu = await client.listMenuItems({ query: { limit: 10 } });
+const order = await client.getOrderById({ path: { orderId: 'ord_01khr…' } });
```
That's the whole client.
From 3bb5fdaffe1e9d129c3602b1207ed0444ab2f13a Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Tue, 1 Sep 2026 17:18:55 +0300
Subject: [PATCH 12/14] docs(blog): scope the zero-deps claim in frontmatter
descriptions too
---
blog/agent-friendly-sdks.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md
index 05aafeb7c..e6e6521af 100644
--- a/blog/agent-friendly-sdks.md
+++ b/blog/agent-friendly-sdks.md
@@ -1,10 +1,10 @@
---
template: ../@theme/templates/BlogPost
title: Open-source, agent-friendly SDKs and tooling from OpenAPI description
-description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents."
+description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, no dependencies beyond each language's HTTP layer, and built for AI agents."
seo:
title: Open-source, agent-friendly SDKs and tooling from OpenAPI description
- description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents."
+ description: "Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, no dependencies beyond each language's HTTP layer, and built for AI agents."
author: roman-marshevskyi
publishedDate: '2026-09-01'
categories:
From 5e6c2cb856a464349d1132932fe1f0c855f06608 Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Tue, 1 Sep 2026 17:20:41 +0300
Subject: [PATCH 13/14] docs(blog): use npx @redocly/cli in the hero diagram
command
---
blog/images/agent-friendly-sdks.svg | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/blog/images/agent-friendly-sdks.svg b/blog/images/agent-friendly-sdks.svg
index eff9b66a3..c4742df3e 100644
--- a/blog/images/agent-friendly-sdks.svg
+++ b/blog/images/agent-friendly-sdks.svg
@@ -29,11 +29,11 @@
-
+ $
- redocly
- generate-client
- → typed · zero deps · yours
+ npx @redocly/cli
+ generate-client
+ → typed · zero deps · yours
From 6bc71abe647502895a874ec3687c68f01d23dcc5 Mon Sep 17 00:00:00 2001
From: Roman Marshevskyi
Date: Tue, 1 Sep 2026 18:43:58 +0300
Subject: [PATCH 14/14] docs(blog): remove duplicate H1 (template renders the
title) and center diagram command
---
blog/agent-friendly-sdks.md | 2 --
blog/images/agent-friendly-sdks.svg | 8 ++++----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md
index e6e6521af..5744461d9 100644
--- a/blog/agent-friendly-sdks.md
+++ b/blog/agent-friendly-sdks.md
@@ -13,8 +13,6 @@ categories:
- api-lifecycle:sdks
---
-# Open-source, agent-friendly SDKs and tooling from OpenAPI description
-
As agents become part of engineering teams, more of your API calls are written by one.
Agents hallucinate endpoints, invent response fields, and hand-write API code you then review line by line.
Generated code is the cheapest, safest code an agent can ship, so we built a generator that treats the agent as a first-class user.
diff --git a/blog/images/agent-friendly-sdks.svg b/blog/images/agent-friendly-sdks.svg
index c4742df3e..f275ed994 100644
--- a/blog/images/agent-friendly-sdks.svg
+++ b/blog/images/agent-friendly-sdks.svg
@@ -30,10 +30,10 @@
- $
- npx @redocly/cli
- generate-client
- → typed · zero deps · yours
+ $
+ npx @redocly/cli
+ generate-client
+ → typed · zero deps · yours