# Revit Templates Sdk

> Author the Nice3point.Revit.Sdk MSBuild SDK that scaffolded Revit add-ins build against, covering its props and targets contract and the C# MSBuild task classes. USE FOR: parsing the configuration into a Revit version and selecting the target framework, emitting REVIT#### and REVIT####_OR_GREATER constants, adding conditional implicit usings, patching the add-in manifest, publishing and repacking output, and supporting .NET compability. DO NOT USE FOR: declaring template options or conditional content in the template package; consuming the SDK to build an add-in.

- Skill: `nice3point/revit-templates-sdk` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nice3point/revit-templates-sdk`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nice3point/revit-templates-sdk/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- License: MIT
- Author: nice3point (https://skillmd.com/u/nice3point)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nice3point/revit-templates-sdk

---


# Revit Templates SDK

`Nice3point.Revit.Sdk` is the custom MSBuild SDK a scaffolded add-in sets through `Sdk="Nice3point.Revit.Sdk"`; the project file stays almost empty and the SDK derives the rest from the active configuration.
It lives in `source/Nice3point.Revit.Sdk`: the props and targets contract in `Sdk/`, and the MSBuild task classes at the project root.
The SDK owns the multi-version build; that logic is never duplicated into template content.

## When to use

- Changing configuration parsing, target-framework selection, or the version constants.
- Changing the implicit usings, manifest patching, publishing, or repacking targets.
- Adding or changing an MSBuild task class in the SDK assembly.

## When not to use

- Declaring a scaffolding option or conditional content. That is template work; see `revit-templates-authoring`.

## Workflow

### Step 1: Keep the props and targets import order

`Sdk/Sdk.props` imports the Microsoft SDK props first, then the SDK's own `Nice3point.Revit.Common.props`; `Sdk/Sdk.targets` imports `Nice3point.Revit.Common.targets`, then the Microsoft SDK targets, then each feature target file.
Order the SDK around the standard build; do not fight it. A feature target hooks the standard build through `AfterTargets` / `BeforeTargets`, never by replacing a built-in target.

### Step 2: Derive the Revit version and target framework in Common.props

`Nice3point.Revit.Common.props` parses the trailing number of the configuration name, normalizes a two-digit form to the full year, and falls back to `-1` when none is present.
`Nice3point.Revit.Common.targets` raises `ValidateRevitVersion` when the version is unresolved; a misnamed configuration fails cleanly.

```xml
<_RevitConfigurationVersion>$([System.Text.RegularExpressions.Regex]::Match($(Configuration), '(\d+)(?!.*\d)').Value)</_RevitConfigurationVersion>
<RevitVersion Condition="'$(_RevitConfigurationVersion.Length)' == '4'">$(_RevitConfigurationVersion)</RevitVersion>
<RevitVersion Condition="'$(_RevitConfigurationVersion.Length)' == '2'">20$(_RevitConfigurationVersion)</RevitVersion>
<RevitVersion Condition="'$(RevitVersion)' == ''">-1</RevitVersion>
```

The `TargetFramework` is then selected by `RevitVersion` thresholds; one project targets the right framework per configuration.

### Step 3: Write an MSBuild task class that returns, never throws

A task class derives from `Microsoft.Build.Utilities.Task`, carries `[PublicAPI]`, and lives at the SDK project root.
Mark an input MSBuild must supply `[Required]` and a value the task hands back `[Output]`.
`Execute` wraps its body in a `try`/`catch`, logs a failure and returns `false`, and returns `true` as a no-op when there is nothing to do; a build step fails cleanly, not by crashing the build host.

```csharp
[PublicAPI]
public class GenerateCompatibleDefineConstants : Task
{
    [Required] public required string Configuration { get; set; }
    public string[] Configurations { get; set; } = [];
    public string? RevitVersion { get; set; }
    [Output] public string[]? DefineConstants { get; private set; }

    public override bool Execute()
    {
        try
        {
            int currentVersion;
            if (string.IsNullOrEmpty(RevitVersion))
            {
                if (!TryGetRevitVersion(Configuration, out currentVersion)) return true;
            }
            else
            {
                if (!int.TryParse(RevitVersion, out currentVersion)) return true;
            }

            //Business logic
            ...

            return true;
        }
        catch (Exception exception)
        {
            Log.LogErrorFromException(exception, false);
            return false;
        }
    }
}
```

This task emits `REVIT####` for the active version and `REVIT####_OR_GREATER` for it and every earlier configuration; template content gates version-specific Revit APIs with `#if REVIT2024_OR_GREATER`.

### Step 4: Register the task and hook it from a feature target

Register each task with `UsingTask` in `Sdk.props`, keyed off the SDK name and the task assembly folder resolved in Step 6.

```xml
<UsingTask TaskName="$(MSBuildThisFileName).GenerateCompatibleDefineConstants" AssemblyFile="$(BuildTasksAssembly)"/>
```

Invoke it from a feature target ordered against the standard build, and route its `[Output]` into the property or item it feeds.

```xml
<Target Name="GenerateRevitCompatibleDefineConstants"
        AfterTargets="AddImplicitDefineConstants"
        Condition="'$(DisableImplicitRevitDefines)' != 'true'">
    <GenerateCompatibleDefineConstants RevitVersion="$(RevitVersion)" Configuration="$(Configuration)" Configurations="$(Configurations)">
        <Output TaskParameter="DefineConstants" ItemName="_ImplicitRevitDefineConstant"/>
    </GenerateCompatibleDefineConstants>
    <PropertyGroup>
        <DefineConstants Condition="'@(_ImplicitRevitDefineConstant)' != ''">$(DefineConstants);@(_ImplicitRevitDefineConstant)</DefineConstants>
    </PropertyGroup>
</Target>
```

### Step 5: Keep the task assembly loadable under every build host

`Nice3point.Revit.Sdk.csproj` multi-targets `net48;netstandard2.0;net10.0`; the assembly loads under MSBuild on .NET Framework, older .NET Core hosts, and the latest SDK.
`Sdk.props` picks the matching folder before registering the tasks.

```xml
<BuildTasksTFM Condition="'$(MSBuildRuntimeType)' == 'Core' AND $([MSBuild]::VersionGreaterThanOrEquals($(NETCoreSdkVersion), '10.0'))">net10.0</BuildTasksTFM>
<BuildTasksTFM Condition="'$(MSBuildRuntimeType)' == 'Core' AND '$(BuildTasksTFM)' == ''">netstandard2.0</BuildTasksTFM>
<BuildTasksTFM Condition="'$(BuildTasksTFM)' == ''">net48</BuildTasksTFM>
```

### Step 6: Verify against the build and keep the README in sync

The SDK package README is sourced from `wiki/MsBuild-Sdk.md` through the csproj `PackageReadmeFile`; a change to the public property surface or a feature updates that wiki page in the same commit.
Verify by running the template test from the `build` directory, which scaffolds projects across the option matrix and compiles each against the SDK.

```shell
dotnet run -- test
```

## Validation

- [ ] `Sdk.props` and `Sdk.targets` keep the import order, and every feature target hooks the standard build through `AfterTargets` / `BeforeTargets`.
- [ ] `RevitVersion` and `TargetFramework` derive from the configuration, and an unresolved version fails through `ValidateRevitVersion`.
- [ ] Every public task class carries `[PublicAPI]`, marks inputs `[Required]` and results `[Output]`, and returns `false` on failure without throwing.
- [ ] Each task is registered with `UsingTask` and invoked from a correctly ordered feature target.
- [ ] The csproj still multi-targets, and `Sdk.props` selects the matching task folder.
- [ ] `wiki/MsBuild-Sdk.md` reflects any public property or feature change, and `dotnet run -- test` compiles the scaffolded matrix.

## Common Pitfalls

| Pitfall                                                      | Correct approach                                                                              |
|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| Throwing from a task on bad input                            | Return `true` as a no-op when nothing applies, and `false` after `Log.LogErrorFromException`. |
| Replacing a built-in target                                  | Hook the standard build with `AfterTargets` / `BeforeTargets` so the SDK orders around it.    |
| Dropping a task TFM from the csproj                          | Keep `net48;netstandard2.0;net10.0`; the assembly loads under every MSBuild host.             |
| Duplicating build logic into template content                | Author configuration parsing, TFM selection, constants, and publishing once in the SDK.       |
| Renaming a public SDK property or task without a deprecation | The SDK surface is a contract; rename only through a deprecation path and keep `[PublicAPI]`. |
| Editing the packed README directly                           | Update `wiki/MsBuild-Sdk.md`; the csproj sources the package README from it.                  |

