Adding a field from a custom table to Dynamics GP report and word template using C#
Extending GP reports using calculated fields and RW functions lets you return custom data (e.g. from SQL or an API) without alternate reports. Use caching to avoid per-line queries for better performance. Word templates need remapping to include new fields.

The microsoft knowledge base article KB number: 888884:
Useful functions for developers to use instead of creating alternate reports in Microsoft Dynamics GP KB: 888884
Tells us about some useful funcations that allow us to inject data into GP native reports and subsequently GP word template reports.
David's site also has some details, and various other blogs on the internet.
RW: Using the rw_TableLineString() and rw_TableLineCurrency() Report Writer functions
It is possible to use these fields together with a Dynamics GP Addin to add extra data from other sources to reports (including potentially websites, AI sources etc).
For the purposes of preserving that inforation in those posts and for my own reference there is a summary below of the fields.
Summary: Dynamics GP Report Writer Integration Functions
To simplify third-party report integrations, Dynamics GP 9.0 introduced six global Report Writer (RW) functions. These allow third-party developers to return custom data to standard reports without creating alternate reports, preserving existing customizations.
These functions are designed for integrations involving parallel third-party tables (e.g., SOP or POP) and are triggered via calculated fields in Report Writer. Each function accepts specific control parameters, which can also be repurposed as needed.
Available Functions
1. rw_ReportStart
Notifies when a report begins.
Parameters:
dict_id
— Dictionary ID of third-party productreport_name
— Name of the report
2. rw_ReportEnd
Notifies when a report ends.
Parameters:
dict_id
report_name
3. rw_TableHeaderCurrency
Returns currency or integer data for a report header.
Parameters:
dict_id
report_name
sNumber
— e.g., SOP or PO NumbersType
— e.g., SOP or PO TypeiControl
— Identifies which field to return
4. rw_TableHeaderString
Same as rw_TableHeaderCurrency
, but returns a string.
Parameters: Same as above
5. rw_TableLineCurrency
Returns currency or integer data for a report line.
Parameters:
dict_id
report_name
sNumber
sType
cSequenceOne
— e.g., Component SequencecSequenceTwo
— e.g., Line Item SequenceiControl
— Identifies which field to return
6. rw_TableLineString
Same as rw_TableLineCurrency
, but returns a string.
Parameters: Same as above
Notes
- These functions avoid the need for alternate reports and support reuse of existing report customizations.
- The
iControl
parameter lets developers return different custom fields with multiple calculated fields referencing the same function.
Add event listeners for these functions from the Functions class
MicrosoftDynamicsGp.Functions.RwTableHeaderString.InvokeBeforeOriginal += RwTableHeaderString_InvokeBeforeOriginal;
MicrosoftDynamicsGp.Functions.RwTableHeaderString.InvokeAfterOriginal += RwTableHeaderString_InvokeAfterOriginal;
Add listeners for those events
// This method is required but left empty – it's triggered after the original RW function runs.
private void RwTableHeaderString_InvokeAfterOriginal(object sender, Microsoft.Dexterity.Applications.MicrosoftDynamicsGpDictionary.RwTableHeaderStringFunction.InvokeEventArgs e)
{
}
// This method runs before the original Dynamics GP rw_TableHeaderString function.
// It allows you to override and return custom data for the report.
private void RwTableHeaderString_InvokeBeforeOriginal(object sender, Microsoft.Dexterity.Applications.MicrosoftDynamicsGpDictionary.RwTableHeaderStringFunction.InvokeEventArgs e)
{
// Check that:
// - The dictionary ID is 0 (meaning core Dynamics GP)
// - The report name matches one of the targeted Purchase Order reports
if (e.inParam1 == 0 &&
(e.inParam2 == "POPPurchaseOrderBlankForm" || e.inParam2 == "POPPurchaseOrderOtherForm"))
{
// Check if the requested field (iControl) is field 0 – adjust logic if more fields are needed
if (e.inParam5 == 0)
{
// Create a database context using your configured connection string
using (var dbContext = new DatabaseSettings(Settings.ConnectionString))
{
// Query your custom PurchaseOrderAdditional table and join with IncotermDetail
// We are selecting the Incoterm description where the PO number matches the control value
var i = dbContext.PurchaseOrderAdditional
.Include(s => s.IncotermDetail)
.AsNoTracking()
.Where(s => s.PONUMBER == e.inParam3)
.Select(s => s.IncotermDetail.Description)
.FirstOrDefault();
// If nothing is found or it’s empty, return an empty string
if (string.IsNullOrWhiteSpace(i))
{
e.result = string.Empty;
}
else
{
// Otherwise, return the Incoterm description as the result
e.result = i;
}
}
}
}
}
So in this example, I'm extending the two Purchase Order reports.
In the calculated field that I’ve added to those reports, I pass the following parameters:
dict_id
:0
(Dynamics main dictionary)report_name
:"POPPurchaseOrderOtherForm"
(Just use a unique string to filter on in your C# code)sNumber
: PO NumbersType
: PO TypeiControl
:0
— identifies which field to return; I just return one field in this case.
This setup now returns the Incoterm
field from our custom SQL table PurchaseOrderAdditional
. The data could just as easily come from an API, an XML file, or anywhere else .NET can access.
This is a really helpful technique.
Word Template
Once you’ve added the calculated field to the GP Report Writer report, remember to export the report as XML. Then, use the Dynamics GP Word Add-in to remap the XML in the Word template to include the new calculated field.
This is no different to adding any calculated field, and there is ample documentation online about how to do this.
Performance and Line Reports
Be aware that these functions execute once for each calculated field and once per line item in the report's detail section.
So, if you're calling into SQL Server or making API requests, a query will fire for every line, which puts significant load on the SQL Server and can noticeably slow down report generation—especially if you’re returning multiple fields per line.
To avoid this, consider fetching all required records in advance—either in the first line or using the rw_ReportStart
function (report header section).
Then use .NET's in-memory caching to hold those results.
In the event listener, pull data from the memory cache rather than triggering a query per line.
Cache entries should be keyed on the report name and header-level composite keys (e.g. PO Number and Type), and given a short expiry (a few seconds is enough). The same user is unlikely to run the same report for the same data more than once within that time frame.
This technique can dramatically improve report performance, especially on large documents.