Create custom HTML widgets using Go’s html/template syntax with built-in query helpers and automatic RBAC enforcement. Widgets are rendered server-side and integrated into pages and dashboards.
Getting Started#
Creating a Widget#
- Navigate to any page or dashboard
- Click Add Widget → Scripted Widget
- Enter a name and description
- Write your Go template code
- Save and preview
Widget Structure#
Every scripted widget consists of:
<!-- Widget parameters (optional) -->
{{define "params"}}
{"itemType": "task", "status": "open"}
{{end}}
<!-- Widget HTML -->
<div class="bg-white rounded-lg shadow p-4">
{{$items := queryItems (printf "type=%s status=%s" .itemType .status)}}
<h3 class="text-lg font-semibold mb-3">Open Items ({{len $items}})</h3>
{{range $items}}
<div class="p-2 border-b">
<span class="font-medium">{{.Title.Get}}</span>
<span class="{{statusColor .Status.Get}} ml-2">{{.Status.Get}}</span>
</div>
{{else}}
<p class="text-gray-500">No items found</p>
{{end}}
</div>
Template Functions#
Query Functions#
| Function | Returns | Description |
|---|---|---|
queryItems(query) | Item[] | Query items with RBAC |
getGroup() | Group | Get current group |
getUser(id) | User | Get user by ID |
getItem(id) | Item | Get item by ID |
Helper Functions#
| Function | Returns | Description |
|---|---|---|
statusColor(status) | string | CSS class for status color |
priorityColor(priority) | string | CSS class for priority |
formatDate(date) | string | Format date for display |
truncate(text, len) | string | Truncate text to length |
Data Access#
Item Fields#
Items have both default fields and custom fields:
<!-- Default fields -->
{{.Title.Get}} <!-- Item title -->
{{.Description.Get}} <!-- Item description (HTML) -->
{{.Status.Get}} <!-- Status ID -->
{{.AssignedTo.Get}} <!-- Assignee user ID -->
{{.DueDate.Get}} <!-- Due date (ISO 8601) -->
{{.ItemType.Get}} <!-- Item type ID -->
{{.CreatedAt.Get}} <!-- Creation date -->
{{.UpdatedAt.Get}} <!-- Last update date -->
<!-- Custom fields -->
{{.GetField "priority"}} <!-- Get custom field value -->
{{.GetFieldHTML "description"}} <!-- Get rendered HTML -->
User Fields#
{{.Name.Get}} <!-- Display name -->
{{.Email.Get}} <!-- Email address -->
{{.FirstName.Get}} <!-- First name -->
{{.LastName.Get}} <!-- Last name -->
{{.ProfilePicture.Get}} <!-- Avatar URL -->
Group Fields#
{{.Name.Get}} <!-- Group name -->
{{.Description.Get}} <!-- Group description -->
{{.Icon.Get}} <!-- Group icon -->
Query Syntax#
Use the Timill query syntax to filter items:
{{/* Simple query */}}
{{$items := queryItems "type = 'task'"}}
{{/* Query with multiple filters */}}
{{$items := queryItems "type = 'task' status = 'open'"}}
{{/* Query with variables */}}
{{$items := queryItems (printf "type = '%s' assignee = '%s'" .itemType .userId)}}
{{/* Complex query */}}
{{$items := queryItems "type = 'task' (status = 'open' OR status = 'in-progress')"}}
Query Operators#
| Operator | Description | Example |
|---|---|---|
= | Equals | status = 'open' |
!= | Not equals | status != 'closed' |
> | Greater than | dueDate > 2024-01-01 |
< | Less than | dueDate < 2024-12-31 |
>= | Greater or equal | |
<= | Less or equal | |
IN | In list | status IN ('open', 'in-progress') |
OR | Logical OR | |
AND | Logical AND (implicit) |
Examples#
Simple Item List#
See full example: examples/scripted-widgets/01_simple_item_list.html
<div class="bg-white rounded-lg shadow">
{{$items := queryItems (printf "type='%s'" .itemType)}}
<h3 class="text-lg font-semibold p-4 border-b">{{.title}} ({{len $items}})</h3>
<div class="divide-y">
{{range $items}}
<div class="p-3 hover:bg-gray-50">
<div class="flex items-center justify-between">
<span class="font-medium">{{.Title.Get}}</span>
<span class="text-sm {{statusColor .Status.Get}}">{{.Status.Get}}</span>
</div>
</div>
{{else}}
<p class="p-4 text-gray-500">No items found</p>
{{end}}
</div>
</div>
Status Dashboard#
See full example: examples/scripted-widgets/02_status_dashboard.html
<div class="grid grid-cols-4 gap-4">
{{$items := queryItems "type = 'task'"}}
{{$open := filter $items "status = 'open'"}}
{{$progress := filter $items "status = 'in-progress'"}}
{{$review := filter $items "status = 'review'"}}
{{$done := filter $items "status = 'done'"}}
<div class="bg-blue-100 p-4 rounded text-center">
<div class="text-2xl font-bold">{{len $open}}</div>
<div class="text-sm">Open</div>
</div>
<div class="bg-yellow-100 p-4 rounded text-center">
<div class="text-2xl font-bold">{{len $progress}}</div>
<div class="text-sm">In Progress</div>
</div>
<div class="bg-purple-100 p-4 rounded text-center">
<div class="text-2xl font-bold">{{len $review}}</div>
<div class="text-sm">Review</div>
</div>
<div class="bg-green-100 p-4 rounded text-center">
<div class="text-2xl font-bold">{{len $done}}</div>
<div class="text-sm">Done</div>
</div>
</div>
Recent Activity#
See full example: examples/scripted-widgets/03_recent_activity.html
<div class="bg-white rounded-lg shadow">
{{$items := queryItems "type = 'task'"}}
{{$recent := sort $items "updatedAt" "desc" | first 5}}
<h3 class="text-lg font-semibold p-4 border-b">Recent Activity</h3>
<div class="divide-y">
{{range $recent}}
<div class="p-3">
<div class="flex items-center space-x-2">
<span class="font-medium">{{.Title.Get}}</span>
<span class="text-xs text-gray-500">{{.UpdatedAt.Get | formatDate}}</span>
</div>
<div class="text-sm text-gray-600">{{.Description.GetHTML | truncate 100}}</div>
</div>
{{else}}
<p class="p-4 text-gray-500">No recent activity</p>
{{end}}
</div>
</div>
RBAC Enforcement#
All queries automatically enforce Role-Based Access Control:
- Users only see items they have permission to view
- Field-level permissions are applied automatically
- Widget rendering respects group membership
Performance Tips#
Use Specific Queries#
<!-- Good: Specific query -->
{{$items := queryItems "type = 'task' status = 'open'"}}
<!-- Bad: Too broad -->
{{$items := queryItems ""}}
Limit Results#
{{/* Use first to limit */}}
{{$recent := first 10 $items}}
Cache When Possible#
{{/* Store in variable to avoid repeated queries */}}
{{$items := queryItems "type = 'task'"}}
{{range $items}}
<!-- Use $items multiple times -->
{{end}}
Security#
| Feature | Description |
|---|---|
| Sandboxed Execution | Templates cannot access raw Go objects |
| RBAC Enforcement | All queries respect permissions |
| Script Timeout | Widgets timeout after 30 seconds |
| No External Requests | Templates cannot make HTTP requests |
| HTML Escaping | All output is automatically escaped |
Troubleshooting#
Widget Shows Blank#
- Check for template syntax errors
- Verify the query returns data
- Check browser console for JavaScript errors
Query Returns No Data#
- Verify item type names are correct
- Check RBAC permissions
- Test with a simpler query
Template Errors#
Common template errors:
<!-- Wrong: Missing quotes -->
{{$items := queryItems type = 'task'}}
<!-- Right: Proper quoting -->
{{$items := queryItems "type = 'task'"}}
Next Steps#
- See Scripting Overview for all scripting environments
- View Examples Gallery for complete examples
- Read API Reference for complete documentation