Advanced MSI
Beyond files and features, MSI supports custom actions, custom tables, install sequence control, merge modules, patches, and transforms. This tutorial covers the advanced authoring features.
Custom Actions
Custom actions let you run code during installation. FalkForge supports several types: DLL entry points, embedded EXEs, and property-setting actions. Each type serves a different purpose.
SetProperty actions
The simplest custom action sets a property value. Use it to pass data between installation phases or configure behavior based on conditions.
package.CustomAction("SetInstallMode", ca =>
{
ca.SetProperty("INSTALL_MODE", "standard");
ca.Condition = Condition.IsInstalling.ToString();
});
The Condition property controls when the action runs. Here it fires only during a fresh install, not during repair or uninstall.
DLL custom actions
For complex logic, embed a native DLL and call a C entry point. First, register the binary. Then reference it in the custom action.
// Embed the DLL as a binary resource
package.Binary("CustomActionsDll", "payload/CustomActions.dll");
// Call a C entry point from the embedded DLL
package.CustomAction("CheckSystemRequirements", ca =>
{
ca.DllFromBinary("CustomActionsDll", "CheckRequirements");
ca.After = "CostFinalize";
ca.Condition = Condition.IsInstalling.ToString();
});
The first argument to DllFromBinary is the binary name. The second is the exported function name in the DLL.
EXE custom actions
You can also run an embedded executable. Pass command-line arguments via the Target property.
package.CustomAction("RunSetupTool", ca =>
{
ca.ExeFromBinary("CustomActionsDll");
ca.Target = "--setup --silent";
ca.Deferred();
ca.NoImpersonate();
ca.After = "InstallFiles";
});
Deferred, rollback, and commit actions
Actions that modify the system must be deferred. Pair them with a rollback action to undo changes on failure. A commit action runs only after successful completion.
// Deferred action — runs during the execution phase
package.CustomAction("ConfigureApp", ca =>
{
ca.SetProperty("CONFIGURE_CMD", "[ProgramFilesFolder]Demo\\app.exe --configure");
ca.Deferred();
ca.NoImpersonate();
ca.After = "InstallFiles";
});
// Rollback action — undoes ConfigureApp on failure
package.CustomAction("UndoConfigureApp", ca =>
{
ca.SetProperty("UNDO_CMD", "[ProgramFilesFolder]Demo\\app.exe --unconfigure");
ca.Rollback();
ca.NoImpersonate();
ca.Before = "ConfigureApp";
});
// Commit action — runs only on success
package.CustomAction("NotifySuccess", ca =>
{
ca.SetProperty("NOTIFY_CMD", "[ProgramFilesFolder]Demo\\app.exe --notify-complete");
ca.Commit();
ca.NoImpersonate();
ca.After = "ConfigureApp";
});
Schedule the rollback action before the deferred action it protects. Windows Installer records rollback actions in reverse order so they run correctly during a rollback.
ContinueOnError
For non-critical actions like telemetry, call ContinueOnError(). The installer proceeds even if the action fails.
package.CustomAction("OptionalTelemetry", ca =>
{
ca.SetProperty("TELEMETRY_CMD", "...");
ca.Deferred();
ca.ContinueOnError();
});
Deep dive: Custom action types and security implications
MSI defines over 50 custom action types. FalkForge exposes the most common ones safely:
- Type 1 (DLL) — calls an exported C function from an embedded DLL. Runs in-process with the installer.
- Type 2 (EXE) — launches an embedded executable as a separate process.
- Type 51 (SetProperty) — sets a property value. No code execution. Safest option.
Security considerations:
NoImpersonate()runs the action as LocalSystem in per-machine installs. Only use it when elevated access is required.- Deferred actions cannot read properties directly. Pass data via
SetPropertyon a property with the same name as the custom action. - Always pair deferred actions with rollback actions. A failed install without rollback leaves the system in a partially configured state.
- DLL custom actions run in the installer process — a crash takes down the entire install. Prefer EXE actions for unreliable third-party code.
Custom Tables
Custom tables store structured data inside the MSI database. Custom actions can read them at install time. They are also useful for embedding metadata that external tools query after deployment.
Defining a schema
Define columns with types and constraints. Mark at least one column as the primary key.
package.CustomTable(ct => ct
.Name("AppConfig")
.Column("Key", CustomTableColumnType.String, col => col.PrimaryKey().Width(72))
.Column("Value", CustomTableColumnType.String, col => col.Width(255))
.Column("Priority", CustomTableColumnType.Int32)
Supported column types: String, Int16, Int32. Use Nullable() for optional columns and Width() to set the maximum string length.
Adding rows
Chain Row() calls to populate the table with data.
.Row(row => row
.Set("Key", "Theme")
.Set("Value", "Dark")
.Set("Priority", 1))
.Row(row => row
.Set("Key", "Language")
.Set("Value", "en-US")
.Set("Priority", 2))
);
When to use custom tables
Use custom tables when you need to:
- Store configuration data that custom actions read at install time.
- Embed deployment metadata (environment names, health-check endpoints, feature flags).
- Drive install-time logic from data rather than hard-coded conditions.
Deep dive: How custom tables are stored in the MSI database
An MSI file is a structured storage database (COM Structured Storage). Each table becomes a stream inside this file. Custom tables are no different from built-in tables like File or Registry — they follow the same column definition format and are queryable via the Windows Installer SQL API.
FalkForge validates custom table identifiers to prevent SQL injection. Table and column names must be valid MSI identifiers: alphanumeric characters, underscores, and periods only. Custom-table identifiers are validated at construction time by TableId.Create and RecipeColumn constructors, with CustomTablesProducer enforcing the same defense-in-depth checks before emission.
You can inspect custom tables after compilation:
forge inspect output.msi
The inspect command lists all tables, including custom ones, with their schemas and row counts.
Sequence Scheduling
MSI runs actions in a defined order called a sequence. FalkForge lets you position custom actions relative to built-in actions using Before() and After().
ExecuteSequence
The execute sequence runs during the actual installation — file copies, registry writes, service operations. Schedule custom actions here when they modify the system.
package.ExecuteSequence(seq => seq
.Action("SetInstallMode")
.After("CostFinalize")
.Condition("NOT Installed")
.Action("RollbackDatabase")
.Before("ConfigureDatabase")
.Condition("NOT Installed")
.Action("ConfigureDatabase")
.After("InstallFiles")
.Condition("NOT Installed")
);
Each Action() call references a previously defined custom action by name. The Condition() call is optional and controls whether the action runs for a given install scenario.
UISequence
The UI sequence runs during the user interaction phase — dialogs, property collection, validation. Use it for actions that gather or display information without modifying the system.
package.UISequence(seq => seq
.Action("PostInstallCleanup")
.After("ExecuteAction")
.Condition(Condition.IsInstalling)
);
Sequence tables
MSI defines four sequence tables:
- InstallExecuteSequence — the main installation sequence. File copies, registry, services.
- InstallUISequence — the UI phase. Dialogs and property validation.
- AdminExecuteSequence — administrative installations (network image creation).
- AdvtExecuteSequence — advertised installations (shortcuts and file associations without files).
FalkForge exposes ExecuteSequence() and UISequence() for the two most common tables. The admin and advertise sequences are rarely needed.
Deep dive: The MSI installation sequence model
An MSI installation runs in two phases:
- UI phase — gathers user input, validates conditions, collects properties. No system changes. Runs the InstallUISequence table.
- Execute phase — performs the actual installation. Runs the InstallExecuteSequence table. Actions here run with elevated privileges in per-machine installs.
Within each phase, actions run in order of their sequence number. After("CostFinalize") places your action immediately after CostFinalize in the sequence. Before("InstallFiles") places it just before InstallFiles.
Key built-in actions and their approximate order:
AppSearch— searches for existing installations.LaunchConditions— checks prerequisites.FindRelatedProducts— detects prior versions.CostFinalize— calculates disk space.InstallInitialize— begins the transaction.RemoveExistingProducts— uninstalls old versions (if scheduled here).InstallFiles— copies files to disk.WriteRegistryValues— writes registry entries.InstallFinalize— commits the transaction.
Deferred actions must be scheduled between InstallInitialize and InstallFinalize. Immediate actions can run anywhere but cannot modify the system.
Merge Modules
A merge module (.msm) packages reusable components that multiple MSI installers can consume. It is the MSI equivalent of a shared library.
Creating a merge module
Use Installer.BuildMergeModule() as the entry point. The API mirrors Installer.Build() but targets the MSM format.
return Installer.BuildMergeModule(args, module =>
{
module.Version(new Version(1, 0, 0));
module.Manufacturer("Demo");
module.Language(1033);
module.Description("Shared components merge module");
// A merge module must include at least one component
module.Component("SharedRuntime");
module.Dependency("SharedRuntime_1.0");
}, (model, outputPath) => new MsmCompiler().Compile(model, outputPath));
The second argument is a compile delegate. MsmCompiler generates the .msm file. The Dependency() call declares a module dependency identifier that consuming MSIs can verify.
Deep dive: When to use MSM vs shared components
Merge modules were the original MSI mechanism for sharing components across products. They work by literally merging tables into the consuming MSI at build time. This has tradeoffs:
- Pros: Self-contained, no runtime dependency resolution, works with any MSI tool chain.
- Cons: No independent servicing. To update the shared component, you must rebuild and redistribute every consuming MSI.
For modern projects, consider alternatives:
- Bundles — chain multiple MSIs together. Each MSI can be updated independently.
- Dependency extension — FalkForge's
Extensions.Dependencyprovides WiX-compatible reference counting via registry keys, allowing shared components to be installed as a separate MSI.
Use merge modules when you need to redistribute a single MSI that includes shared components, or when your deployment infrastructure requires it.
Patches
A patch (.msp) updates an installed MSI without requiring a full reinstall. It contains only the differences between two MSI versions.
Creating a patch
Use Installer.BuildPatch(). Provide the baseline MSI (what the user has installed) and the target MSI (the updated version).
return Installer.BuildPatch(args, patch =>
{
patch.Manufacturer("Demo");
patch.Description("Updates App from 1.0 to 1.1");
patch.TargetMsi("payload/app-v1.msi");
patch.UpdatedMsi("payload/app-v2.msi");
patch.Classification(PatchClassification.Hotfix);
patch.AllowRemoval();
}, (model, outputPath) => new PatchCompiler().Compile(model, outputPath));
TargetMsi() specifies the baseline. UpdatedMsi() specifies the new version. The compiler diffs the two and produces the patch. AllowRemoval() lets users uninstall the patch to revert to the baseline.
Deep dive: Patch classification and versioning rules
MSI patches have three classifications:
- Hotfix — a targeted fix. Changes only specific files or registry entries. Does not bump the product version.
- ServicePack — a cumulative update. May change many files. Typically bumps the minor version.
- Update — a general-purpose update. Broadest scope.
Versioning rules for patches:
- The
PackageCode(GUID) must differ between the baseline and target MSIs. - The
ProductVersionshould change. The first three fields (Major.Minor.Build) are significant to Windows Installer. - The
UpgradeCodemust remain the same. - The
ProductCodecan stay the same for minor updates or change for major updates.
Patches are applied via msiexec /p patch.msp or programmatically through the Windows Installer API.
Transforms
A transform (.mst) modifies an MSI's properties and behavior without changing the MSI file itself. IT departments use transforms to customize vendor MSIs for their environment.
Creating a transform
Use Installer.BuildTransform(). Reference the base MSI and override properties as needed.
return Installer.BuildTransform(args, transform =>
{
transform.BaseMsi("payload/base.msi");
transform.Description("Enterprise Customization");
// Override properties for enterprise deployment
transform.SetProperty("ALLUSERS", "1");
transform.SetProperty("INSTALLDIR", @"D:\Apps\MyApp");
transform.SetProperty("REBOOT", "ReallySuppress");
}, (model, outputPath) => new TransformCompiler().Compile(model, outputPath));
BaseMsi() identifies the MSI to customize. SetProperty() overrides property values. Common overrides include install directory, per-user vs per-machine scope, and reboot suppression.
Applying transforms
Apply a transform during installation via the command line:
msiexec /i product.msi TRANSFORMS=enterprise.mst
Or via Group Policy by attaching the MST to the MSI deployment. The transform modifies the MSI's behavior without altering the original file.
Deep dive: Enterprise deployment with transforms
Transforms are the standard mechanism for customizing vendor MSIs in enterprise environments. Common use cases:
- Silent deployment — set
REBOOT=ReallySuppressandALLUSERS=1for SCCM/Intune deployment. - Custom install paths — override
INSTALLDIRto match your organization's directory structure. - Feature selection — pre-select or deselect features so users get a standardized configuration.
- License keys — embed license information so users are not prompted.
Transforms preserve the vendor's digital signature on the MSI. Since the MSI file is not modified, its signature remains valid. The transform is a separate file that Windows Installer applies at runtime.
You can stack multiple transforms: TRANSFORMS=base.mst;dept.mst. They apply in order, with later transforms overriding earlier ones.
Recap
| Feature | Demo | Entry Point / Key API |
|---|---|---|
| Custom Actions | 09, 20 | package.CustomAction() — SetProperty, DllFromBinary, ExeFromBinary, Deferred, Rollback, Commit |
| Custom Tables | 09, 26 | package.CustomTable() — Name, Column, Row |
| Sequence Scheduling | 09, 28 | package.ExecuteSequence(), package.UISequence() — Action, Before, After, Condition |
| Merge Modules | 44 | Installer.BuildMergeModule() — MsmCompiler |
| Patches | 45 | Installer.BuildPatch() — TargetMsi, UpdatedMsi, PatchCompiler |
| Transforms | 46 | Installer.BuildTransform() — BaseMsi, SetProperty, TransformCompiler |