Skip to content
Back to skills

Trace Form

ASecurity

Trace how an Oracle Forms screen actually works — which form opens a modal, where a button's code really lives when its body looks empty, what a :GLOBAL carries back into a field. Use when asked how a Forms form, block, item, button, modal window or LOV behaves, where a value comes from, what calls what, or when a trigger comes back with no body.

  • 3 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
ai-agentsrustshellsqlgitdatabase

Works with

  • cursor
  • mcp

Security analysis

A100/100

Scanned September 19, 2026

npx -y skills add aoreshkov/oracle-forms-mcp --skill trace-form --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Trace Form?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Trace Form
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aoreshkov-trace-form/badge)](https://www.skillsdirectory.com/skills/aoreshkov-trace-form)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: trace-form
description: Trace how an Oracle Forms screen actually works — which form opens a modal, where a button's code really lives when its body looks empty, what a :GLOBAL carries back into a field. Use when asked how a Forms form, block, item, button, modal window or LOV behaves, where a value comes from, what calls what, or when a trigger comes back with no body.
---

# Trace a Forms interaction

Tracing a Forms application is following one value across objects that hide where they are defined.
The `oracle-forms` MCP server answers every step of that; this is the order to ask in, and the five
places where a naive reading is not merely incomplete but **wrong**.

Tool names below are the plain ones. Through this plugin they are namespaced —
`mcp__plugin_oracle-forms_oracle-forms__get_block` — which is the form to use in permission rules,
a subagent's `tools` list, or a hook matcher.

## Read through the tools, not the files

The converted XML and the extracted PL/SQL live in a cache the server owns and fingerprints against
the `.fmb`. Opening those files directly — `grep`, `sed`, a directory walk — reads text that may
describe a form that is no longer the one being served; that drift is exactly what a `STALE` status
reports. The paths in a result are cache-relative by design, so they are not host paths you could
open anyway.

A `STALE` row's `staleReason` says which kind of drift it is: `SOURCE_CHANGED` (the `.fmb` moved on)
or `INDEX_OUTDATED` (an older build of the server indexed it — its answers can be wrong in ways
that read as plausible, such as an inherited trigger reported empty). Both are healed by
`fetch_module`; the second re-parses the converted file and runs no conversion. After upgrading the
server, `list_modules(status="stale")` is the one call worth making before trusting a warm cache.

Every reflex has a call:

| Reflex | Call instead |
|---|---|
| `ls`/`find` the forms directory | `list_modules` with `pattern`, `type`, `status` (filtered and paged) |
| `grep` a module for an item, prompt or property | `get_block` (`verbosity: "detailed"` for data type, column, canvas, LOV and the DML properties) |
| `grep` across the whole directory | `search_modules` (every fetched module at once) |
| `grep` one attribute across a block | `search_source` with `scope: "xml"` — one call, with line numbers |
| `sed -n '320,340p'` on the XML | `get_object_xml` for one named object, `read_source` for a line range |
| open the sidecar a hit named | `read_source` with the hit's `uri` |

## The order to ask in

1. **`list_modules`** — find the module. Filter; never page through thousands of rows.
2. **`fetch_module`** — converts and indexes it. Idempotent, and every other tool needs it first.
3. **`get_module_overview`** with `verbosity: "detailed"` — the inventory, plus the window and
   canvas objects: **modality**, sizes, toolbar canvases, and which window hosts which canvas.
4. **`get_block`** — items with type, **property class**, prompt, trigger names, and subclassing
   pointers. This is where a screen's fields become legible. *What can this screen see* starts
   here too: `block.relations`/`detailOf` (with `preventMasterlessOperations`, a detail is reached
   only through its master), then the master's `PRE-QUERY` and `dml.whereClause`.
5. **`list_triggers`** → **`get_trigger`**, **`list_program_units`** → **`get_program_unit`** — the
   PL/SQL. `list_triggers` filters by block, item or level, which is how same-named triggers at
   different levels are told apart — and `get_trigger` takes the same `level` (or an exact
   `ownerPath`, `:FORM` for form level). While the module attaches a library that is not fetched,
   a body's `hint` says so: a routine it calls that the module does not define may live there, and
   what that call does is undetermined until the library is read. A body comes back in a file of its own, so its line numbers
   count from the first line of *that body*: cite them as `POST-INSERT:75`, never `ORDERS.fmb:75`,
   which is a line nobody opening the form will find. (A `.pll` is the exception — its units are
   ranges within the one `.pld` dump of the whole library.)
6. **`search_source`** inside one module; **`search_modules`** when the question leaves it.
7. **`read_source`** — the lines around anything a result pointed at, by its `uri` alone (the URI
   names its module, so `module` is not needed with it). Over converted
   XML ask for a few dozen lines at a time: one line is one whole object and can run to thousands
   of characters. A cut page says so, gives `nextStartLine`, and its `hint` is the next call —
   continue from *that*, not from where you assumed the page ended.

## Five things that mislead if you skip them

**An empty PL/SQL body is not "this does nothing."** A subclassed object stores only its overrides,
so its body is genuinely empty *here* while the code that runs lives in the parent module. Check
`bodySource` on every body you read: `own`, `inherited`, `resolved`, `empty`. When it is
`inherited`, the `inherited` pointer names the parent module and the object's name and `ownerPath`
*there*, and `hint` names the exact next call. `get_trigger`/`get_program_unit` take `resolve: true`
to follow it, but only into a module that is **already fetched** — so fetch the parent first.

**Modality lives on the Window, not on the block or canvas.** "How does this modal work" starts at
`get_module_overview(verbosity: "detailed")`: a modal window is a dialog, which means the code that
fills it and the code that consumes its result sit on opposite sides of one interaction, usually in
different modules.

**An item's property class is usually its role.** Forms shops name classes for what the object does,
so a push button is an *LOV* button and a text item is a *filter* field only because of the class it
inherits. `get_block` reports `propertyClass` on items and blocks; without it every button looks
alike.

**A property Forms did not write is `null`, not `false`.** Forms2XML emits a property only where it
differs from the default, so `visible`, `required`, `modal`, `raiseOnEnter` and friends are nullable:
absence means "not overridden", never "off".

**And on a classed item, `null` is not the default either — the class decides it.** In a real form
most items write almost no properties of their own; `DatabaseItem`, `InsertAllowed`,
`UpdateAllowed`, `Enabled` come from the property class, which is usually a stub pointing at a
shared module. So *"which fields does this insert write?"* is
`get_block(verbosity: "detailed")` → **`effectiveDml`**, the item's properties with its class
applied, not `items[].dml`, which is only what the item itself wrote. An item appears in
`effectiveDml` only when its class could be followed into a **fetched** module; every item that is
not is listed in `unresolvedItems` with the module to fetch — an item absent from the map is an
unknown, never a default. The same holds for *how big* a field is: a classed item writes a width and
no height at all, so ask `effectiveGeometry`, not `items[].width`/`height`, which are only what the
item itself wrote. (Neither is the length it accepts — that is `maximumLength`.) `items: [...]` narrows the call to the fields a question is about. `columns: true` adds the
base table's columns, the ones no item supplies, and which of *those* are mandatory — the columns
an insert fails on unless a trigger assigns them.

## Where is a value written

*"Where does `:ORDERS.CUSTOMER_NAME` get its value?"* is the question that structures most tracing,
and the one most often answered wrongly — because the obvious search covers a fraction of the ways
Forms writes a field, and a search that matches nothing looks like an answer.

There is no `find_writes` tool: nothing here parses PL/SQL, so this is `search_source` plus knowing
what to search for. Cover the PL/SQL shapes — each is a separate search, and the last two are not
reachable by searching for the item's name at all:

| Shape | What to search |
|---|---|
| assignment | `:=` next to the name, but also the name alone — spacing varies (`:ORDERS.CUSTOMER_NAME:=`) |
| query into a variable | `into` (`SELECT … INTO :orders.customer_name`, `FETCH … INTO`, `RETURNING … INTO`) |
| string-addressed | `copy(` — `Copy(v, 'ORDERS.CUSTOMER_NAME')` names the item in a *string*, which a search for `:orders.customer_name` never matches |
| item properties | `set_item_property`, `set_block_property` (a `DEFAULT_WHERE` rewrites what the block queries) |
| globals | `:global.` — a global is written in one module and read in another |
| through an argument | an OUT / IN OUT parameter: the call site passes the item, the write is inside the procedure |
| built at runtime | a name concatenated into a string: unreachable lexically, by construction |

Then the **declarative** writers, which no PL/SQL search can see at all:

- **the query itself** — a database item is populated by every fetch; `get_block(verbosity:
  "detailed")` → `effectiveDml.databaseItem`;
- **`copyValueFromItem`** and **`initialValue`** (Forms' `InitializeValue`) — both in
  `effectiveDml`, and on a classed item they usually come from the class, not the item;
- **relation join keys** — Forms copies the master's value into the detail's join item on every
  coordination: `get_block` → `relations`/`detailOf` and read `joinCondition`;
- **an LOV return item** — the index keeps the LOV's name (`items[].lovName`) and its column names,
  but not which item each column returns into. `get_object_xml(objectType: "LOV", name: …)` is the
  call for that.

Two rules that cost a wrong claim if skipped:

- **A zero-hit pattern proves the pattern absent, not the write.** A regex like
  `(customer_name|…)\s*:=` returning nothing says nothing about a
  `SELECT … INTO :orders.customer_name`. A search that found nothing says so itself now —
  `filesSearched` with a `hint` naming what that scope cannot reach — but it cannot know which
  shapes you did not try. Before concluding "written only here", check that the searches above were
  actually run, and read `total` and `files`, which count the whole module, not just the page you
  were shown.
- **In `scope: "xml"`, `<` is a literal `<`.** The search runs over the file's own text, where
  element markup is written with real angle brackets, so a pattern written `&lt;Relation` can only
  match a file that literally spells that — it is a silently dead branch, and in an alternation a
  sibling that *does* match hides it.

## Questions that leave the module

`search_modules` searches every **fetched** module in one call. Three shapes cover most of tracing:

```text
search_modules "ORDERS"                                   → which forms call this one
search_modules ":GLOBAL.picked_ref"                       → who writes and who reads a global
search_modules 'ParentFilename="toolbar.fmb"' scope=xml   → who subclasses a shared block
```

Matching is case-insensitive by default, because the same module is written `PICKER`, `picker` and
`Call_Form('picker')` in one code base. Read the coverage fields before trusting an empty answer:
`cachedModules`, `scannedModules`, `skippedNotCached`, `skippedStale`. Only fetched modules are
searched, so a thin result may mean "not fetched yet" — the `hint` names the `fetch_module` call
that widens the search, and names the **attached libraries** of the modules it did search: a
procedure a form calls but does not define is most often in one of those `.pll` files. Some of those
libraries are not in the forms directory at all; the hint says so separately, and there is no call
for that case — a routine defined in one of them is **undetermined**, not absent, and a claim about
what it does rests on nothing until the library is served. When `truncated` is set, pass the
returned `nextCursor` back with the same arguments.

## When `get_object_xml` is the right call

It is the escape hatch, and it is legitimate when:

- a property no tool models yet decides the answer (it returns the raw attributes);
- you are confirming an attribute a result summarised;
- the object is a vendor- or version-specific element the parser passed through generically.

It is the wrong call for item prompts and properties (`get_block`), trigger bodies (`get_trigger`),
finding anything (`search_source`/`search_modules`), or reading a range of a file (`read_source`).
Fragments are capped and say so when cut; a subclassed object's fragment also carries the
inheritance pointer, because Forms writes the parent attributes on the *enclosing* owner and the
fragment alone could not answer it.

## A worked trace

Against the repository's own `sample-forms` directory — *how does the value picked in the modal get
into the field on the calling form?*

1. `fetch_module ENTRY.fmb`
2. `get_trigger ENTRY.fmb KEY-HELP block=ASSIGNMENTS item=AGENT_REF` → the body calls
   `call_form('picker')`
3. `fetch_module picker` — the name out of the PL/SQL is a usable module spec
4. `get_module_overview PICKER.fmb verbosity=detailed` → `WIN_PICKER` is `modal: true`, toolbar canvas
   `BAR_LIST`
5. `get_block PICKER.fmb block=BAR_LIST` → the block is subclassed from `TOOLBAR`; item `SELECT`
   carries `ownerPath: BAR`
6. `get_trigger PICKER.fmb WHEN-BUTTON-PRESSED block=BAR_LIST item=SELECT` → empty body,
   `bodySource: inherited`, hint names `fetch_module(module="TOOLBAR.fmb")`
7. `fetch_module TOOLBAR.fmb`, then the same `get_trigger` with `resolve: true` → the real body,
   `Do_Key('HELP')`
8. `list_triggers PICKER.fmb` → `KEY-HELP` at block level on `CUSTOMERS`; `get_trigger` it → it
   writes `:GLOBAL.picked_name` / `:GLOBAL.picked_ref` and exits the form
9. `search_modules ":GLOBAL.picked_ref"` → the calling form reads it back into
   `:ASSIGNMENTS.AGENT_REF`
10. `read_source` with that hit's `uri` → the line in context

Not one step needed a file. If a trace of your own seems to, say what was missing rather than
reaching for the shell — that is a gap in the server, and it is the kind that gets fixed.

## Record what you worked out

A trace is expensive and the conclusions are not in the `.fmb` — what a naming convention means,
which property class marks a base-table item, which field is validated but never written.
`annotate_element` stores one annotation on one element; `relate_elements` records a directed
cross-reference (this trigger *calls* that program unit; this button *opens* that form). Both
survive re-indexing and cache eviction, and the read tools surface them inline the next time anyone
looks. The annotation's sort is the `kind` argument (`note`, `tag`, `summary`, `classification`),
its text is `body`, and which object it is about is `elementKind` + `name` (+ `ownerPath`):

```
annotate_element(module="ORDERS.fmb", elementKind="program_unit", name="CALC_TOTAL",
                 kind="summary", body="Sums line amounts; commits via Forms_Ddl.")
relate_elements(module="ORDERS.fmb", fromKind="trigger", fromName="WHEN-BUTTON-PRESSED",
                fromOwner="B1.PB_OK", toKind="program_unit", toName="CALC_TOTAL", relType="calls")
```

A call with a wrong or missing argument is not run, and its error lists every problem at once with
the values each argument accepts — fix them all from that one message.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.