cycls.Web configures everything the user sees and the identity behind it. Pass
it to @cycls.agent(web=...).

The empty-chat screen uses the brand name, description and logo.
Branding
name, description, logo and brand are per locale, so call .brand()
again with "ar" for Arabic. Image paths are read at build time, SVGs inlined
and rasters embedded as data URIs, so assets do not need to ship with the
container.
Pulling copy from a CMS
.brand() call overrides the CMS for exactly that
field and no other.
Theme and colors
primary drives highlights and active states, secondary drives chips and
bubbles. The _dark variants override dark mode and default to the light values.
Fonts are two CSS variables: --font-display for the agent’s name, the names in
the agents dropdown and page titles, and --font-sans for everything else. Both default to the system font
stack, and --font-display follows --font-sans until you set it. Set them from
.head(), and serve the font files with .copy_public():
The empty-chat screen
- Suggestions
- Example gallery
SEO and social
.seo() covers the page title and meta description when they should differ from
the brand copy. .head() appends raw HTML to <head> and is repeatable.
Every agent also serves /og.png, a generated social preview, plus
/robots.txt, /sitemap.xml and /llms.txt.
Mobile app
https:// store URL.
On iOS or Android, a bar under the header reads “Cycls App, For a better
experience” with an Open button to that platform’s link. The bar names the
Cycls app and shows its icon, whatever your branding. Dismissing it or tapping
Open hides it for 14 days.
Each browser’s own prompt comes first:
- An App Store link adds the
apple-itunes-appmeta tag, so Safari on iPhone and iPad shows Apple’s banner instead of the bar. - A Play link serves
/manifest.webmanifest, so on Android, Open can bring up Chrome’s install sheet.
Static files
https://your-agent.cycls.ai/public/assets/logo.png.
Uploads
Voice input
The mic in the composer records, and your server turns the recording into text with a speech-to-text model. The browser sends audio and gets the words back. It never learns which service or model heard them, and never holds a key..voice() names the model as vendor/model, the way cycls.LLM does, with the
service’s base_url. Any service that speaks OpenAI’s audio transcriptions API
works:
.voice(), the mic uses OpenAI’s gpt-4o-transcribe whenever
OPENAI_API_KEY is set, and is hidden when it is not. Every vendor but OpenAI
needs a base_url. The key is api_key, or <VENDOR>_API_KEY from the server’s
environment. Use headers= for an endpoint that authenticates another way, such
as a proxy.
Through OpenRouter, the same call reaches ElevenLabs, Deepgram, AssemblyAI and
Google models, and open ones such as Whisper, Voxtral and Nemotron. List them with
GET https://openrouter.ai/api/v1/models?output_modalities=transcription.
Analytics and push
.analytics(True) is shorthand for the PostHog default. Providers are plugins
over one canonical event pipe, and each can be scoped with an events allowlist
of the names below. .notifications() adds web push, where the permission card
is ours and the trigger is a platform decision.
PostHog identifies signed-in people with their name, email, organization and
plan. The plan is the token’s pla claim, the same value as context.user.plan.
A u: plan sets user_plan and user_is_paid, and an o: plan sets org_plan
and org_is_paid, so switching between personal and an organization updates only
that pair. Paid means any plan except u:free_user and o:free_org.
Events
Events
Reacting to finished runs
error_id and taken_back are present only when they apply.
Method reference
Next
Authentication
Clerk, any OIDC provider, and what lands in
context.user.