DEV Community

Wuic Framework
Wuic Framework

Posted on • Originally published at wuic-framework.com

The chatbot's toolbox: the actions an LLM can apply to your app

A chatbot that only answers questions is a search box with manners. Ours does more: it can propose concrete changes to the app you're looking at. You describe what you want in plain language, the model picks the right tool, and you get a proposal — a chip with an Apply button, the generated code, the target route, and the model's rationale, all visible before anything happens.

If you know MCP (the Model Context Protocol), the mental model is the same: hand the model a catalogue of typed tools and let it call them. We define the tools inline against the Anthropic tool-use API rather than behind an MCP server, but the shape is identical — a name, a description that tells the model when to use it, and a typed argument list. The model never touches a database; it produces a structured intent and the framework owns the execution.

One rule sits above all of them: nothing is applied without a click. The model proposes, you dispose.

Media — overview clip: asking the chatbot for an action and the resulting "Apply" chip appearing under the answer.

Below is the catalogue, one tool at a time, each with the kind of prompt that triggers it.

Grid actions

propose_toolbar_action

Adds a custom button to a list grid's toolbar, operating on the current selection, with a generated JavaScript callback.

You:  "on the orders grid add a button that exports the selected rows to CSV"

Bot:  toolbar action   Apply
      route:    orders
      callback: const rows = datasource.getSelectedRows();
                const csv = toCsv(rows); download('orders.csv', csv);
Enter fullscreen mode Exit fullscreen mode

Demo propose_toolbar_action — bulk archive on cities

propose_row_action

Adds a button to a single row (in the row's action dropdown), with a callback that receives that record.

You:  "put an Approve button on each row of the requests grid"

Bot:  row action   Apply
      route:    requests
      callback: await fetch('/api/requests/approve', {... id ...});
                await datasource.fetchData();
Enter fullscreen mode Exit fullscreen mode

Demo propose_row_action — per-row PDF generation

Conditional styling

propose_table_style

Applies a CSS class to a whole row when a JS condition is true (a row-level highlight).

You:  "highlight in red the rows whose due date is in the past"

Bot:  table style   Apply
      class:     row-danger
      condition: new Date(record.due_date) < new Date()
Enter fullscreen mode Exit fullscreen mode

Demo propose_table_style — conditional row coloring

propose_column_style

Same idea, scoped to a single cell instead of the whole row.

You:  "give the status cell a green background when it's 'OK'"

Bot:  column style   Apply
      column:    status
      class:     cell-success
      condition: record.status === 'OK'
Enter fullscreen mode Exit fullscreen mode

Demo propose_column_style — single-cell conditional coloring

Cell rendering and form titles

propose_display_formula

Replaces a column's rendered value in the list with a custom HTML/Angular template — a badge, an icon, a coloured number.

You:  "show priority as a coloured badge: green/yellow/red"

Bot:  display formula  ▸ Apply
      column:  priority
      template: <span class="badge" [class.high]="...">{{ priority }}</span>
Enter fullscreen mode Exit fullscreen mode

Media — screenshot: the priority column rendered as coloured badges.

propose_form_title_formula

Computes the edit-form's title dynamically from the record being edited.

You:  "the edit form title should read 'Customer {company_name}'"

Bot:  form title formula   Apply
      route:  customers
      formula: `Customer ${record.company_name.value}`
Enter fullscreen mode Exit fullscreen mode

Media — clip: opening a record and seeing the dynamic title in the dialog header.

Form callbacks

propose_default_value_callback

Generates the callback that prefills a field when a new record is opened.

You:  "default created_date to today on new records"

Bot:  default value   Apply
      column:   created_date
      callback: record[field.mc_nome_colonna] = new Date().toISOString().slice(0,10);
Enter fullscreen mode Exit fullscreen mode

Media — screenshot: the insert form opening with the date prefilled.

propose_custom_validation

Generates a validation that blocks the save when a rule — possibly cross-field — isn't met, with a message.

You:  "validate that population can't be negative, block the save with a message"

Bot:  custom validation   Apply
      column:   population
      callback: if (record[field.mc_nome_colonna].value < 0) {
                  vr.message = 'Population cannot be negative'; return false;
                } return true;
Enter fullscreen mode Exit fullscreen mode

Media — clip: typing a negative value, hitting Save, and the validation blocking it.

propose_selection_changed

Hooks the change of a lookup/select field to recompute or prefill other fields.

You:  "when the customer lookup changes, copy its VAT number into the record"

Bot:  selection changed   Apply
      column:   customer
      callback: record.vat_number.next(record.customer__lookup_obj.value?.vat ?? '');
Enter fullscreen mode Exit fullscreen mode

Media — clip: picking a customer and watching the VAT field auto-fill.

propose_lifecycle_callback

Hooks a record lifecycle event — before/after save, after load — for a side effect.

You:  "before save, uppercase the city name"

Bot:  lifecycle callback   Apply
      event:    before_save
      callback: record.CityName.next(record.CityName.value.toUpperCase());
Enter fullscreen mode Exit fullscreen mode

Media — clip: saving a lowercase name and seeing it stored uppercase.

Metadata

propose_simple_metadata_update

Patches a simple metadata field — page size, a caption, hide-in-list/edit, a basic flag — without opening the metadata editor.

You:  "set the cities grid page size to 50"

Bot:  metadata update  ▸ Apply
      route:  cities
      field:  page size → 50
Enter fullscreen mode Exit fullscreen mode

Media — screenshot: the grid paging at 50 rows after Apply.

propose_metadata_column_create

Creates a new column on a route — including a computed (non-physical) one.

You:  "add a computed column total_chars = LEN(city_name)"

Bot:  create column  ▸ Apply
      route:    cities
      column:   total_chars (number, computed)
      formula:  LEN([Application].[Cities].[CityName])
Enter fullscreen mode Exit fullscreen mode

Media — screenshot: the new computed column showing in the grid.

SQL (super-admin)

propose_sql_metadata_field

Writes a raw SQL fragment into a metadata field — a custom JOIN, a computed expression, a custom SELECT clause — concatenated at runtime into the auto-generated queries. The model generates the fragment in the active provider's dialect (MSSQL / MySQL / PostgreSQL / Oracle quoting and syntax).

You:  "on the orders total column, compute price × quantity"

Bot:  SQL metadata field   Apply   (super-admin)
      target:  orders.total (computed expression)
      sql:     [price] * [quantity]
Enter fullscreen mode Exit fullscreen mode

This is the only tool gated harder than a click: it requires super-admin privileges server-side and writes an audit log row on every application.

Media — screenshot: the computed total column + the audit log entry.

Designer canvas

propose_designer_inject

The one tool that does not write metadata. On the dashboard designer page it injects or edits components on the canvas — the same in-memory tree the drag-and-drop UI manipulates. Nothing persists until you click "Save dashboard", and the designer's undo/redo covers the model's edits exactly like a human's.

You:  "add a grid bound to cities"   (on the designer page)

Bot:  designer inject  ▸ Apply
      injects: DATASOURCE + DATAREPEATER, bound and configured for route 'cities'
Enter fullscreen mode Exit fullscreen mode

It knows the full catalogue of canvas tools and their editable properties, and it fuzzy-matches sloppy route names ("provincie"stateprovinces) before committing to an argument.

Demo propose_designer_inject — master-detail injection in designer canvas

Before it acts: asking for the schema

Every tool above needs real names — the actual column name on the route, the SQL schema and table, the join alias behind a lookup. Early versions handed all of that to the model upfront: the page context was a full dump of every column on the current grid. It bloated every request, action or not.

So we flipped it. The page context is now minimal — just the route you're on, e.g. cities/list. When a prompt needs schema the model doesn't already have, it calls one more tool first:

request_metadata_detail — a non-chip, behind-the-scenes tool. {detail: 'columns', route} returns the real column list and the SQL identity (schema, table, the full qualifier to use in a formula); {detail: 'lookup_columns', route, column} returns a lookup's join alias and its related columns. The framework resolves it — the model still never touches the database — hands the result back, and then the model emits the propose_* with names it looked up instead of names it guessed.

You:  "show the province lookup as 'id - name'"

Bot:  (calls request_metadata_detail  learns the join alias
       is StateProvinceID_stateprovinces) then:
      SQL metadata field   Apply
      sql:  CAST([StateProvinceID_stateprovinces].[StateProvinceID] AS VARCHAR(10))
            + ' - ' + [StateProvinceID_stateprovinces].[StateProvinceName]
Enter fullscreen mode Exit fullscreen mode

It's the same trust property as everywhere else, one level deeper: the model can't reference a column or an alias that doesn't exist, because it asks before it acts — and the prompt stays lean, fetching only what the question needs.

The quiet tools

Three tools never produce a chip — they shape the conversation instead:

  • remember_fact / forget_fact — the model pins high-priority facts it shouldn't lose ("this project uses snake_case columns"). They survive summarisation.
  • suggest_followups — the clickable chips under each answer that propose the next question.

Media — screenshot: a memory fact pinned in the header + follow-up chips under an answer.

Why a fixed toolbox

A typed catalogue over a metadata-driven framework is a good fit precisely because the framework already has a small, safe, well-defined surface of "things you can configure". The model never writes arbitrary code that runs unsandboxed; it fills in the blanks of operations the framework was already designed to perform — and you approve each one with your eyes on the preview.

The chatbot, and this whole action layer, ships with every WUIC install and runs on the public demo. Open it on a list grid and ask it to colour the overdue rows red. Read the chip. Then click Apply. For how the engine behind it is built and served, see the architecture post.

Top comments (0)