Varendo

Quickstart: one dataset, four questions, four views

Describe a People dataset once, then ask for a list, a filter, a monthly trend, and a per-team chart. Varendo picks the view for each question.

Describe your data once. Then ask questions about it — “show everyone”, “how many people joined each month?” — and Varendo picks a view that fits each answer: a table, cards on a narrow screen, a line chart, or a bar chart.

Choose this setup when the same data needs several views selected from typed requests. For a complete read-only array already owned by React, start with the local surface API. If your screen already chooses its views and owns its interaction state, use standalone components.

Varendo 0.1.0 is the published stable release. Install all Varendo packages at the same exact version.

1. Create a React app

Shell example
npm create vite@latest people -- --template react-ts
cd people

This creates a people project and moves you into it.

2. Install Varendo

Keep every Varendo package on the same exact version:

Shell example
npm install --save-exact @varendo/core@0.1.0 @varendo/runtime@0.1.0 @varendo/web@0.1.0 @varendo/react@0.1.0 zod@4.5.4

You should see: all four Varendo packages at 0.1.0 in package.json.

3. Describe your data

Create src/people.ts. This file answers three questions, once:

TypeScript example
import { defineResource } from '@varendo/core';
import { createLocalDataService } from '@varendo/runtime/data';
import { createAeliqoApp } from '@varendo/web/app';
import { z } from 'zod';

const rows = [
  { id: 'ada', name: 'Ada Chen', team: 'Design', joined: '2026-01-12' },
  { id: 'sam', name: 'Sam Rivera', team: 'Engineering', joined: '2026-02-03' },
  { id: 'jo', name: 'Jo Patel', team: 'Engineering', joined: '2026-02-24' },
  { id: 'kim', name: 'Kim Osei', team: 'Product', joined: '2026-03-15' },
  { id: 'max', name: 'Max Weber', team: 'Design', joined: '2026-05-06' },
];

const people = defineResource({
  id: 'people',
  revision: '1',
  label: 'People',
  identity: ['id'],
  schema: z.object({ id: z.string(), name: z.string(), team: z.string(), joined: z.iso.date() }),
  fields: {
    name: { label: 'Name' },
    team: { label: 'Team', role: 'dimension' },
    joined: { label: 'Joined', role: 'time' },
  },
  measures: { hires: { label: 'New hires', aggregate: 'count' } },
  presentation: { allowedViews: ['table', 'cards', 'trend', 'bar'] },
});

// Your app owns access. Return the same scope from the data and the app.
const access = { scopeDigest: 'people:all', policyRevision: '1' };

export function createPeopleApp() {
  const data = createLocalDataService({
    snapshot: { catalog: people.catalog, sourceRevision: '1', records: { people: rows } },
    authorize: () => ({ ok: true, value: access }),
  });
  return createAeliqoApp({
    resources: [{ resource: people, data }],
    authority: {
      read: () => ({
        ok: true,
        value: {
          principalKey: 'local-user',
          ...access,
          experienceRevision: '1',
          grants: ['task.evaluate', 'result.inspect', 'experience.commit'],
          readContext: {},
        },
      }),
    },
  });
}

allowedViews is the complete list of views Varendo may choose for this data. It never shows a view you did not list.

4. Ask questions with intents

An intent is a small, typed request: what to show, from which resource. It says nothing about markup. Replace src/main.tsx with this file:

React TypeScript example
import { StrictMode, useState } from 'react';
import { createRoot } from 'react-dom/client';
import type { Intent } from '@varendo/core';
import { AeliqoProvider, AeliqoRegion } from '@varendo/react/app';
import { createPeopleApp } from './people';
import './people.css';

const questions = {
  Everyone: { version: '1', id: 'everyone', kind: 'browse', resource: 'people' },
  'Engineering only': {
    version: '1',
    id: 'engineering',
    kind: 'browse',
    resource: 'people',
    filter: { op: 'compare', field: 'team', comparison: 'eq', value: 'Engineering' },
  },
  'Hires per month': {
    version: '1',
    id: 'hires-per-month',
    kind: 'analyze',
    resource: 'people',
    measures: [{ id: 'hires', revision: '1' }],
    time: { field: 'joined', grain: 'month' },
  },
  'Hires per team': {
    version: '1',
    id: 'hires-per-team',
    kind: 'analyze',
    resource: 'people',
    measures: [{ id: 'hires', revision: '1' }],
    dimensions: ['team'],
  },
} satisfies Record<string, Intent>;

type Question = keyof typeof questions;
const app = createPeopleApp();

function App() {
  const [question, setQuestion] = useState<Question>('Everyone');
  return (
    <AeliqoProvider app={app}>
      <main>
        <h1>People</h1>
        <div className="questions" role="group" aria-label="Ask about people">
          {(Object.keys(questions) as Question[]).map((name) => (
            <button key={name} type="button" aria-pressed={name === question} onClick={() => setQuestion(name)}>
              {name}
            </button>
          ))}
        </div>
        <AeliqoRegion regionId="people" resourceId="people" intent={questions[question]} />
      </main>
    </AeliqoProvider>
  );
}

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

Then create src/people.css for the page layout and buttons. Varendo's views bring their own styles and follow your system's light or dark theme.

CSS example
:root {
  color-scheme: light dark;
  font:
    100% / 1.5 system-ui,
    sans-serif;
}
main {
  max-width: 60rem;
  margin: auto;
  padding: clamp(1rem, 4vw, 3rem);
}
.questions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-block: 1.5rem;
}
.questions button {
  min-height: 44px;
  padding: 0.625rem 1rem;
  border: 1px solid ButtonText;
  border-radius: 0.5rem;
  font: inherit;
}
.questions button[aria-pressed='true'] {
  color: HighlightText;
  background: Highlight;
}
.questions button:focus-visible {
  outline: 3px solid Highlight;
  outline-offset: 3px;
}

5. Run it and ask

Shell example
npm run dev

Open the URL printed by Vite, then press each button:

QuestionIntentWhat you see
EveryonebrowseA table of five people.
Engineering onlybrowse + filterSam and Jo. Varendo evaluated the filter; it did not hide rows in the DOM.
Hires per monthanalyze + timeA line chart of hires from January to May.
Hires per teamanalyze + dimensionsA bar chart: Design 2, Engineering 2, Product 1.

In your running React app, narrow the browser window while Everyone is selected. The same table becomes cards when its container is compact, and returns when it widens.

You never chose a component. Each answer came from one data definition, and Varendo selected an allowed view that fits the result and the available space.

What just happened

  1. Validate. Varendo checked each intent against people: the resource, fields, filter, and hires measure must all exist.
  2. Evaluate. It asked your data service for the permitted rows and computed the answer, such as hires grouped by month.
  3. Present. It picked a view from allowedViews that suits the answer — a time series becomes a trend, one category per bar becomes a bar chart.

The same intent can come from your code, a button, a URL, or an AI agent. An agent gets exactly the same checks and can never send HTML, code, or permissions. See Connect an agent when you are ready.

If the result differs

What to add next