---
title: "Display Construction — Canvas, Dashboard, Dynamics, Symbols & CodeBehind"
tags: [display, canvas, dashboard, symbol, navigation, hmi, ui, layout, dynamics, codebehind]
description: "Build displays from scratch: Canvas and Dashboard layouts, zone-based Canvas positioning, symbol placement with SymbolLabel binding, visual dynamics (color, visibility, rotation), ActionDynamic click handling, CodeBehind for display logic, and asset-driven navigation"
version: "3.0"
author: "Tatsoft"
---
Build complete FrameworX displays: choose between Canvas and Dashboard layouts, place controls and symbols with proper tag binding, wire visual dynamics (color, visibility, rotation based on tag values), configure ActionDynamic for navigation, write CodeBehind for display lifecycle events, and set up asset-driven navigation patterns.
New in v3: Zone-based Canvas layout methodology that produces professional, well-spaced process overview screens without manual adjustment.
Use when:
Do NOT use when:
get_objects + write_objects)| Category | Items |
|---|---|
| Tools | get_table_schema, write_objects, get_objects, list_elements, list_dynamics, browse_runtime_properties |
| Tables | DisplaysList, DisplaysSymbols, DisplaysLayouts |
Critical: Use DisplaysList for creating and editing displays. DisplaysDraw is the Designer visual editor UI — NOT a writable table.
Before building any display, use these tools to discover what's available:
list_elements() -- all element types by category + library folders
list_dynamics() -- ALL dynamic types by category (Color, Visibility, Action, Animation, etc.)
list_elements('Symbol/HMI') -- browse symbol library
list_elements('Canvas') -- Canvas display structure schema
list_elements('Dashboard') -- Dashboard display structure schema
list_dynamics() with no parameter is the door to the entire dynamics system — it lists every available dynamic type organized by category with minimal examples.
Fetch the display schema and decide Canvas vs Dashboard:
get_table_schema('DisplaysList')
list_elements('Canvas') or list_elements('Dashboard')
Canvas — absolute positioning (Left/Top/Width/Height). Use for:
Dashboard — responsive grid (Columns/Rows/Cells). Use for:
PanelType property is REQUIRED — omitting it silently defaults to Canvas, which causes confusing results if you intended a Dashboard.
Decision guide: If the display is primarily data monitoring (gauges, trends, grids), prefer Dashboard — it handles spacing automatically. Use Canvas only when process flow diagrams, P&ID schematics, or custom shapes/lines are needed. For mixed needs: Dashboard for the main monitoring page, Canvas for process overview pages.
AI agents consistently produce poorly laid out Canvas displays when placing elements with arbitrary coordinates. Always use the zone-based approach described here.
NEVER start by placing individual elements. Instead:
Before writing ANY element, mentally divide the 1366×728 canvas into rectangular zones. Each zone represents a process area, equipment group, or information panel.
Standard zone layout for a process overview (1366×728):
PAGE TITLE (x:0, y:0, w:1366, h:50) ACTION BTNs | ||||||
|---|---|---|---|---|---|---|
| Zone 2 | Zone 2 | Zone 2 | Zone 2 | Zone 2 | Zone 2 |
BOTTOM PANEL: Trend, Alarms, KPIs, or ML Status (h:300+) | ||||||
Note: Page-to-page navigation buttons belong in the Header display (see Step 10b), not on the Canvas title bar. ACTION BTNs here are for in-page actions only (e.g., start/stop, acknowledge, reset).
Zone sizing rules:
For N process stages on a 1366×728 canvas:
Available width = 1366 - 40 (left/right margin 20px each) = 1326
Available height = 728 - 50 (title) - 20 (top margin) = 658Zone width = (Available width - (N-1) × 15) / N Zone height = 320 (for process row) Zone startY = 60 (below title)
Zone[i].Left = 20 + i × (zoneWidth + 15) Zone[i].Top = 60 Zone[i].Width = zoneWidth Zone[i].Height = 320
Bottom panel: Left = 20, Top = 400, Width = 1326, Height = 658 - 320 - 20 = 318
Example: 6 process stages
Zone width = (1326 - 5×15) / 6 = 208px eachZone 0: Left=20, Top=60, W=208, H=320 Zone 1: Left=243, Top=60, W=208, H=320 Zone 2: Left=466, Top=60, W=208, H=320 Zone 3: Left=689, Top=60, W=208, H=320 Zone 4: Left=912, Top=60, W=208, H=320 Zone 5: Left=1135, Top=60, W=208, H=320
Bottom: Left=20, Top=400, W=1326, H=308
Every process zone MUST have a background Rectangle for visual grouping. Use themed colors — never hardcode colors for zone backgrounds.
Standard zone background — use PanelBackground theme:
{
"Type": "Rectangle",
"Left": 20, "Top": 60, "Width": 208, "Height": 320,
"FillTheme": "PanelBackground",
"Stroke": "#FF909090", "StrokeThickness": 1
}
Higher contrast zones (rare — popups, callouts): use PopupBackground theme. Not needed for normal process zones.
Page background should use PageBackground theme (set on the display properties, not as a rectangle).
CRITICAL theming rules for zone backgrounds:
FillTheme: "PanelBackground" — this respects Light/Dark theme switching automatically"Fill": "#0A000000" — these break in Dark themeFill + FillTheme: "" when you need process-specific meaning (e.g., water blue for a tank zone)Zone backgrounds must be placed FIRST in the Elements array so they render behind all other content.
Every process zone follows the same internal layout pattern. Coordinates are offsets from the zone's Left/Top origin:
|
Symbol centering formula:
Symbol.Left = Zone.Left + (Zone.Width - Symbol.Width) / 2
NEVER put value and unit in separate TextBlocks side by side. Instead, use ONE TextBlock with composite LinkedValue:
{
"Type": "TextBlock",
"LinkedValue": "Flow: {@Tag.Plant/Intake/FIT_1001} GPM",
"Left": 30, "Top": 150, "Width": 170, "Height": 24,
"FontSize": 14
}
Patterns:
"Flow: {@Tag.X} GPM" — label + value + unit"{@Tag.X} NTU" — value + unit only"pH: {@Tag.X}" — label + value (no unit)Show process flow direction between zones using TextBlock arrows:
{
"Type": "TextBlock",
"Text": "→",
"Left": 228, "Top": 190, "Width": 15, "Height": 30,
"FontSize": 20
}
Arrow placement: horizontally centered in the gap between zones, vertically aligned with the symbol center.
The bottom panel (below process zones), can be created as required by information to show on the dwaplsy, or by default, use one of the following options::
Option A: Trend + KPI sidebar
Bottom Panel | |
|---|---|
TrendChart (w:700, h:280) |
|
Option B: Trend + Alarm split
Bottom Panel | |
|---|---|
TrendChart (w:660, h:280) |
|
Option C: Full-width trend with status bar
Bottom Panel (h:30) | |
|---|---|
Status: Score: 0.023 Confidence: 0.98
|
Bottom panel sections should also use PanelBackground theme background rectangles.
| Element | Minimum Size | Recommended |
|---|---|---|
| Wizard Symbol | 70×70 | 80×80 to 100×100 |
| Library Symbol | 60×60 | 80×80 |
| Section Title | FontSize 14 | FontSize 15–16 |
| Value Text | FontSize 13 | FontSize 14 |
| Unit Label | FontSize 11 | FontSize 12 |
| Gauge (Circular) | 150×150 | 180×180 |
| TrendChart | 300×180 | 500×200 |
| AlarmViewer | 400×150 | 600×200 |
| Button | 100×35 | 130×40 |
Dashboard uses a grid with DashboardDisplay (columns/rows definition) and Cells array.
You never set Width or Hight on Dashboard, just the cell
list_elements('Dashboard')
{
"table_type": "DisplaysList",
"data": [{
"ObjectName": "MainPage",
"PanelType": "Dashboard",
"DashboardDisplay": {
"Columns": ["*", "*", "*"],
"Rows": ["*", "*"]
},
"Cells": [
{
"Row": 0, "Col": 0,
"Cell": { "HeaderLink": "Tank 1 Level" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Level.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 1,
"Cell": { "HeaderLink": "Tank 1 Temp" },
"Content": {
"Type": "CircularGauge",
"LinkedValue": "@Tag.Plant/Tank1/Temperature.Value",
"Minimum": 0, "Maximum": 100
}
},
{
"Row": 0, "Col": 2,
"Cell": { "HeaderLink": "Trend" },
"Content": {
"Type": "TrendChart",
"Duration": "5m",
"Pens": {
"Type": "TrendPenList",
"Children": [
{ "Type": "TrendPen", "LinkedValue": "@Tag.Plant/Tank1/Level", "PenLabel": "Level", "Stroke": "#FF2196F3", "Auto": true }
]
}
}
},
{
"Row": 1, "Col": 0, "ColSpan": 3,
"Cell": { "HeaderLink": "Equipment Status" },
"Content": {
"Type": "Symbol",
"SymbolName": "Wizard/TANK",
"SymbolLabels": [
{ "Type": "SymbolLabel", "Key": "Value", "LabelName": "Value", "LabelValue": "@Tag.Plant/Tank1/Level.Value", "FieldType": "Expression" }
]
}
}
]
}]
}
Key Dashboard rules:
DashboardDisplay defines the grid: Columns and Rows arrays (use "*" for proportional sizing)Cell references Row/Col (0-based) and contains a Content elementColSpan/RowSpan for multi-cell elementsCell.HeaderLink adds a header label to the cellBrowse available symbols:
list_elements('Symbol') -- all symbol categories
list_elements('Symbol/HMI') -- HMI symbol library
list_elements('Symbol/HMI/Equipment') -- equipment symbols
list_elements('Library') -- all library folders
list_elements('Wizard') -- Wizard symbol schema
Symbols are reusable components with parameterized inputs. When you place a symbol, you use SymbolLabels to connect the symbol's internal @Label. parameters to actual @Tag. values.
@Label.State, @Label.Speed, etc.@Label. key to a real @Tag. value.Concrete example — placing a Pump symbol:
{
"Type": "Symbol",
"SymbolName": "Wizard/PUMP",
"Left": 200, "Top": 300, "Width": 80, "Height": 80,
"SymbolLabels": [
{ "Type": "SymbolLabel", "Key": "State", "LabelName": "State", "LabelValue": "@Tag.Plant/Pump1/Running.Value", "FieldType": "Expression" },
{ "Type": "SymbolLabel", "Key": "Speed", "LabelName": "Speed", "LabelValue": "@Tag.Plant/Pump1/Speed.Value", "FieldType": "Expression" }
]
}
CRITICAL: Use @Tag. bindings in SymbolLabels. NEVER use @Label. when placing symbols in displays — @Label. is only for symbol internal definitions (in DisplaysSymbols).
WizardSymbols — always available, no import needed: TANK, VALVE, PUMP, MOTOR, BLOWER. Place with SymbolName: "Wizard/PUMP".
Library symbols (~1,600 available) auto-import when referenced by SymbolName. No manual import step needed — just reference the path (e.g., "HMI/Equipment/CentrifugalPump").
Sizing: Symbols are vector-based and scale to any proportional size. Use 40x40 for compact, 80x80 for medium, 120x120 for large — the library default is just a starting point. On Canvas, always use minimum 80×80 for process overview displays (see Step 3 minimum sizes).
Dynamics attach runtime behaviors to ANY element. They decouple the visual effect from the data binding, so any property of any control can become live.
Discovery: Call list_dynamics() with no parameter to see all available dynamic types, or narrow by category:
list_dynamics() -- all dynamics by category
list_dynamics('Color') -- color-changing dynamics
list_dynamics('Visibility') -- show/hide dynamics
list_dynamics('Animation') -- rotation, position, scaling
list_dynamics('Action') -- click/navigation dynamics
list_dynamics('FillColorDynamic') -- full schema for a specific dynamic
The most common visual dynamic. Changes an element's fill color based on a tag value.
Pattern: A Dynamics array on the element, containing one or more dynamic objects.
{
"Type": "Rectangle",
"Left": 100, "Top": 200, "Width": 60, "Height": 60,
"Dynamics": [
{
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Running",
"ChangeColorItems": {
"Type": "ColorChangeList",
"Children": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
{ "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF00FF00" }
]
}
}
]
}
For analog values (temperature, pressure), use graduated thresholds:
{
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Tank1/Temperature",
"ChangeColorItems": {
"Type": "ColorChangeList",
"Children": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF2196F3" },
{ "Type": "ChangeColorItem", "ChangeLimit": 60, "LimitColor": "#FFFFEB3B" },
{ "Type": "ChangeColorItem", "ChangeLimit": 80, "LimitColor": "#FFFF9800" },
{ "Type": "ChangeColorItem", "ChangeLimit": 95, "LimitColor": "#FFF44336" }
]
}
}
{
"Type": "TextBlock",
"Text": "ALARM",
"Left": 100, "Top": 50, "Width": 80, "Height": 25,
"Dynamics": [
{
"Type": "VisibilityDynamic",
"LinkedValue": "@Tag.Plant/Tank1/AlarmActive"
}
]
}
Element is visible when the linked value is non-zero/true, hidden when zero/false.
{
"Type": "Symbol",
"SymbolName": "Wizard/BLOWER",
"Left": 200, "Top": 100, "Width": 80, "Height": 80,
"Dynamics": [
{
"Type": "RotationDynamic",
"LinkedValue": "@Tag.Plant/Fan1/Speed",
"MinAngle": 0, "MaxAngle": 360,
"MinValue": 0, "MaxValue": 100
}
]
}
Elements can have multiple dynamics simultaneously:
{
"Type": "Rectangle",
"Left": 100, "Top": 200, "Width": 60, "Height": 60,
"Dynamics": [
{
"Type": "FillColorDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Running",
"ChangeColorItems": {
"Type": "ColorChangeList",
"Children": [
{ "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF808080" },
{ "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FF4CAF50" }
]
}
},
{
"Type": "VisibilityDynamic",
"LinkedValue": "@Tag.Plant/Pump1/Enabled"
}
]
}
Always verify dynamic schemas: Call list_dynamics('FillColorDynamic') (or whichever type) for the exact property names and structure.
For button/navigation behavior:
list_dynamics('ActionDynamic')
Simple format — single action, auto-mapped to MouseLeftButtonDown:
{
"Type": "Button",
"Text": "Go to Details",
"Left": 50, "Top": 400, "Width": 150, "Height": 40,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "DetailPage"
}
}
Nested format — multi-event, explicit mouse event mapping:
{
"ActionDynamic": [
{
"Event": "MouseLeftButtonDown",
"Actions": [
{ "Type": "NavigateToDisplay", "DisplayName": "DetailPage" }
]
}
]
}
Displays are document objects — full replacement on write.
Creating a new display: Write the full object as shown in Steps 2–4.
Modifying an existing display — read first, modify, write back:
get_objects('DisplaysList', names=['MainPage'], detail='full')
MainPage is predefined in new solutions. Write main content directly into it — no need to create a new display for the landing screen.
CodeBehind is CLIENT-SIDE C# or VB.NET code embedded in each display for lifecycle events and operator interaction.
Lifecycle methods:
DisplayOpening() — runs once when the display loadsDisplayIsOpen() — runs cyclically while displayed (interval set by IsOpenInterval)DisplayClosing() — runs once when the display closesDialogOnOK() — runs when a dialog's OK button is clickedCodeBehind is stored in the Contents field of the display, formatted as {Language}\r\n{Code}.
Minimal example — a button click that writes a tag value:
{
"table_type": "DisplaysList",
"data": [{
"ObjectName": "ControlPage",
"PanelType": "Canvas",
"Size": "1366 x 728",
"Contents": "CSharp\r\nvoid DisplayOpening()\r\n{\r\n // Initialize display state\r\n}\r\n\r\nvoid ButtonStart_Click(object sender, EventArgs e)\r\n{\r\n @Tag.Plant/Pump1/Command.Value = 1;\r\n}\r\n\r\nvoid ButtonStop_Click(object sender, EventArgs e)\r\n{\r\n @Tag.Plant/Pump1/Command.Value = 0;\r\n}",
"Elements": [
{
"Type": "Button",
"Text": "Start Pump",
"Left": 50, "Top": 100, "Width": 120, "Height": 40,
"ClickEvent": "ButtonStart_Click"
},
{
"Type": "Button",
"Text": "Stop Pump",
"Left": 200, "Top": 100, "Width": 120, "Height": 40,
"ClickEvent": "ButtonStop_Click"
},
{
"Type": "TextBlock",
"LinkedValue": "{@Tag.Plant/Pump1/Running.Value}",
"Left": 50, "Top": 160, "Width": 200, "Height": 25
}
]
}]
}
Key CodeBehind rules:
Contents field format: first line is language (CSharp or VBdotNet), followed by \r\n, then code@ prefix for runtime object access in code: @Tag.Path/Name.Value, @Client.Context.AssetPathSee the Scripts and Expressions skill for full CodeBehind and server-side scripting guidance.
Layout regions: Header, Footer, Menu, Submenu, Content.
get_table_schema('DisplaysLayouts')
get_objects('DisplaysLayouts', names=['Startup'], detail='full')
The Startup layout defines which display loads into the Content region (typically MainPage). Only modify if you need:
Layouts are document objects — read first, modify, write back. Dependencies: DisplaysSymbols → DisplaysList → DisplaysLayouts
When a solution has multiple content pages (Dashboard, ProcessOverview, AlarmPage, TrendPage, etc.), navigation buttons belong in the Header display — not on each content page.
Why the Header: The Header region is persistent — it stays visible while the Content region changes. Placing navigation here means every page gets consistent navigation without duplicating buttons on each content page.
Workflow:
get_objects('DisplaysLayouts', names=['Startup'], detail='full')
get_objects('DisplaysList', names=['Header'], detail='full')
{
"Type": "Button",
"Text": "Dashboard",
"Left": 900, "Top": 5, "Width": 110, "Height": 32,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "MainPage"
}
},
{
"Type": "Button",
"Text": "Process",
"Left": 1020, "Top": 5, "Width": 110, "Height": 32,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "ProcessOverview"
}
},
{
"Type": "Button",
"Text": "Alarms",
"Left": 1140, "Top": 5, "Width": 110, "Height": 32,
"ActionDynamic": {
"Type": "NavigateToDisplay",
"DisplayName": "AlarmPage"
}
}
Button placement in the Header:
CRITICAL: Do NOT put page navigation buttons on content pages (Canvas or Dashboard). Content pages should only contain buttons for in-page actions (start/stop, acknowledge, open popup). Page-to-page navigation is always in the Header.
Exception: If the solution has NO layout (single standalone page), navigation buttons can go on the content page itself — but this is unusual for production solutions.
Common pattern for plant-wide navigation with dynamic content:
Architecture:
Client.Context updates → content displays reactStatic binding (fixed tag path):
@Tag.Area1/Line1/State
Dynamic binding (resolves based on selected asset):
Asset(Client.Context.AssetPath + "State1")
This allows a single display template to show data for whichever asset the operator selects in the navigation tree.
Setting Up Asset Navigation:
Plant/Line1/Motor1, Plant/Line2/Motor1 with same member names)Asset() syntax in display bindings for dynamic resolutionbrowse_runtime_properties('Client') at design time to explore the Client namespace and Context propertiesWhen setting custom colors on elements, be aware that theme overrides can silently replace your custom values at runtime.
Key theme colors for displays:
| Theme Name | Purpose | Usage |
|---|---|---|
PageBackground | Display page background | Set on display properties |
PanelBackground | Zone/section backgrounds | Default for zone Rectangles |
PopupBackground | Higher contrast panels | Use for callouts, modal-like zones |
ControlBackground | Control backgrounds | Auto-applied to gauges, charts |
TextForeground | Text color | Auto-applied to TextBlocks |
DefaultBorder | Border color | Auto-applied to control borders |
Best practice: OMIT color properties to use themed defaults. Only specify colors when intentionally overriding for process-specific meaning (alarm red, water blue, etc.).
Pattern — set color AND clear theme (when you must override):
{
"Fill": "#FF3498DB",
"FillTheme": ""
}
If you only set Fill without clearing FillTheme, the theme engine may override your color. Always clear the corresponding theme property when setting custom colors.
Use list_elements('ThemeColors') for available named theme brushes (StateOK, StateAlarm, Water, AlarmHighPriority, etc.).
Before writing any Canvas display, verify:
FillTheme: "PanelBackground""Flow: {@Tag.X} GPM")PanelBackground theme Rectangles| Mistake | Why It Happens | How to Avoid |
|---|---|---|
Using DisplaysDraw as table_type | Confused with DisplaysList | DisplaysDraw is the visual editor UI, not a writable table |
| Omitting PanelType | Defaults silently to Canvas | Always set PanelType explicitly |
Using @Label. in display elements | Confused with symbol definitions | @Label. is only for DisplaysSymbols internals. Use @Tag. when placing symbols |
| Not discovering dynamics first | Doesn't know what's available | Call list_dynamics() with no parameter to see all dynamic types |
| Screenshots for self-validation | Habit from other tools | Trust write_objects success. User sees live updates |
| Setting colors without clearing theme | Theme overrides custom colors | Set value AND clear theme: {Fill: '#FF3498DB', FillTheme: ''} — or just omit to use themed defaults |
| Sending partial display content | Forget it's a document object | Always read-modify-write for existing displays |
| Canvas positioning in Dashboard | Mixing layout paradigms | Canvas uses Left/Top + Elements; Dashboard uses Row/Col + Cells |
| Referencing symbols with wrong path | Incomplete library path | Browse with list_elements('Symbol/HMI') to get exact paths |
| Guessing dynamic property names | Different dynamics have different schemas | Always call list_dynamics('DynamicTypeName') for exact schema |
| CodeBehind in wrong Contents format | Missing language prefix | Contents must start with CSharp\r\n or VBdotNet\r\n before code |
| All Canvas content in top-left | No zone planning | Calculate zones FIRST to fill the full canvas |
| Symbols at 40×40 or 50×50 | Copying library defaults | Minimum 80×80 on process overview displays |
| Value and Unit as separate TextBlocks | Seems logical but misaligns | Single TextBlock: "{@Tag.X} GPM" |
| No background rectangles on zones | Doesn't know about visual grouping | Always add Rectangle with FillTheme: "PanelBackground" |
| Hardcoded fill colors on zone backgrounds | Breaks in Dark theme | Use FillTheme: "PanelBackground" instead |
| FontSize 10–11 for values | Default seemed fine | Minimum 13, prefer 14 for operator readability |
| Page navigation buttons on content pages | Seems convenient per-page | Navigation goes in the Header display (Step 10b) — content pages only have in-page action buttons |
| Display Action | Tool Call |
|---|---|
| Get display schema | get_table_schema('DisplaysList') |
| Get Canvas structure | list_elements('Canvas') |
| Get Dashboard structure | list_elements('Dashboard') |
| Get element schema | list_elements('ElementName') |
| Browse all dynamics | list_dynamics() |
| Get specific dynamic schema | list_dynamics('FillColorDynamic') |
| Browse symbols | list_elements('Symbol/HMI') or list_elements('Library') |
| Browse Wizard symbols | list_elements('Wizard') |
| Browse theme colors | list_elements('ThemeColors') |
| Read existing display | get_objects('DisplaysList', names=['PageName'], detail='full') |
| Write display | write_objects('DisplaysList', data=[...]) |
| Navigate Designer to display | designer_action('navigate', 'Display.PageName') |
| Browse runtime properties | browse_runtime_properties('Client') |
| Read Header for nav buttons | get_objects('DisplaysList', names=['Header'], detail='full') |
| Read layout structure | get_objects('DisplaysLayouts', names=['Startup'], detail='full') |
search_docs('scripts', labels='skill')search_docs('new solution', labels='skill')list_protocols() to browse, then search_docs('modbus', labels='connector') for protocol-specific docssearch_docs('custom symbols', labels='tutorial') for building reusable parameterized symbols