System Composer logo

System Composer

Organization
matlab
system-composer

Use this skill when authoring reusable, idempotent MATLAB scripts that build System Composer architecture models via the architecture-modeling API — `systemcomposer.createModel`, `addComponent`, `addPort`, `setInterface`, `connect(srcPort, dstPort)`, interface dictionaries (.sldd) with `addInterface`/`addElement`, profiles/stereotypes with `Profile.createProfile` and `addStereotype`, or `systemcomposer.allocation.createAllocationSet`. Also trigger when debugging these APIs (connections that don't appear, interfaces that don't resolve, profile save errors, `createAllocationSet` signature-mismatch errors). Do NOT trigger for ad-hoc structural edits to an already-built model (adding one SubSystem, rewiring a port) — use `building-simulink-models` with `model_edit` for that.

Overview

Publishermatlab
Repositoryagent-skills-playground
Skill namesystem-composer
Stars
179
Forks
32
Bundled files
8
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 8 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by matlab on GitHub. Read the source before you install it.

Installation

Install the System Composer AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/matlab/agent-skills-playground.git /tmp/agent-skills-playground
mkdir -p .claude/skills
cp -r /tmp/agent-skills-playground/demos/mbse-with-agentic-ai/skills/system-composer .claude/skills/system-composer
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable System Composer in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads System Composer on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so System Composer is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

MATLAB System Composer — Programmatic Authoring Guide

System Composer lets you model multi-domain architectures in MATLAB. This skill captures the correct API patterns, common gotchas, and a proven script structure for building models reliably.


When to use this skill vs. building-simulink-models (SATK)

Both skills can edit System Composer .slx files. They work at different API layers — don't mix them in one script.

ConcernThis skill (architecture-modeling API)building-simulink-models with model_edit (block-diagram API)
Component creationaddComponent(arch, "Name") — returns systemcomposer.Componentadd_block with type: "SubSystem" — returns a blk_id
PortsaddPort(arch, "Name", "in", iface) — typed, interface-awareBus Element blocks (In Bus Element / Out Bus Element) inside the SubSystem
Connectionsconnect(srcPort, dstPort) — port objects{"op": "connect", "target": "blk_X.y1 -> blk_Y.PortName"}
Interface dictionaries (.sldd)First-class (createDictionary, addInterface, setInterface)Not addressed
Profiles / stereotypesFirst-class (Profile.createProfile, addStereotype, applyStereotype)Not addressed
Allocation sets (.mldatx)First-class (systemcomposer.allocation.createAllocationSet)Not addressed
Auto-layoutCall Simulink.BlockDiagram.arrangeSystem explicitly before save — programmatic adds all land at (0,0)model_edit runs autolayout automatically; its guardrail forbids manual arrangeSystem

Use this skill when:

  • Writing an idempotent buildMyModel.m / buildMyArchitecture.m script that will be re-run from scratch
  • The architecture uses interface dictionaries, stereotypes/profiles, or allocation sets — model_edit has no primitives for any of these
  • Debugging SC-specific API failures (CST connect shadow, composite ArchitecturePort errors, dict.save + re-fetch, profile.save path, createAllocationSet signature mismatch)

Defer to building-simulink-models with model_edit when:

  • Making a one-off structural change to an already-built SC model (add one SubSystem, rewire one port, tweak a parameter)
  • The user just wants "add a component called X" and the model has no interface dictionary / profile / allocation set the change needs to stay consistent with
  • The MBSE workflow in mbse-workflow is not involved

Do not mix in one script. model_edit adds components via add_block with type: "SubSystem"; this skill adds them via addComponent. The two produce different object types and the architecture-modeling APIs in this skill (setInterface, applyStereotype, addPort) may not work on SubSystem-block-created components. Pick one layer per script.


Recommended Script Structure

Keep profile creation in the same script as the architecture — add it at the end, after the model and connections are built:

buildMySystemModel.m    ← architecture + interface dictionary + profile/stereotypes

This keeps both artifacts in sync on every rebuild, and avoids the "profile already applied" uniqueness error that occurs when a separate profile script re-applies a profile to an already-profiled model.

The script is idempotent: it deletes and recreates all artifacts on every run.


Phase 1+2: Architecture Model + Interface Dictionary

Skeleton

See code/buildMySystemModel.m for the full parameterized function:

buildMySystemModel(modelName, dictFile, archDir)

Phase 3: Profile & Stereotypes

See code/buildMySystemProfile.m for the full parameterized function:

buildMySystemProfile(profileName, modelName, archDir)

Critical API Gotchas

These will silently fail or throw cryptic errors without warning:

What you wantCorrect APIWrong / common mistake
Connect two component portsconnect(srcPort, dstPort)connect(arch, srcPort, dstPort) — silently fails; dispatches to Control System Toolbox
Wire a composite boundary port to a sub-component portconnect(boundaryArchPort, subComp.getPort("Name")) — boundary is the saved ArchitecturePort ref; sub-component is its ComponentPort via getPortConnecting two ArchitecturePorts across the boundary, or using the boundary's ComponentPort — both throw "incompatible directions"
Assign interface to portport.setInterface(iface)port.Interface = iface — read-only property error
Add interface elementaddElement(iface, "Name", Type="double")Type="MyValueTypeName" — value type names resolve to Simulink.ValueType objects which the bus compiler cannot use; always use a Simulink base type directly
Create modelsystemcomposer.createModel(name)systemcomposer.createModel(name, true) — invalid 2nd arg
Set string stereotype propertysetProperty(comp, path, '"value"')setProperty(comp, path, 'value') — evaluates as MATLAB variable
Set string default in addPropertyDefaultValue='"standard"'DefaultValue="standard" — evaluates, throws error
Apply stereotype propertysetProperty(comp, ...)setPropertyValue(comp, ...) — wrong function name
Close all profilessystemcomposer.profile.Profile.closeAll()...closeAll("-discard") — too many arguments
Create profilecreateProfile(name)createProfile(name, "file.xml") — invalid 2nd arg
Look up a component by pathKeep component vars when adding, or use resolveComponent(arch, 'Parent/Child') helperarch.lookup('Path', ...) — does not exist
Create allocation setcreateAllocationSet(name, srcModelName, dstModelName) — model namescreateAllocationSet(name, srcModelObj, dstModelObj) — model objects fail on R2025b with unhelpful AllocationAppCatalog signature error

Why connect(srcPort, dstPort) Must Have No Architecture Argument

connect is shadowed by Control System Toolbox's connect.m. When you write connect(arch, srcPort, dstPort), MATLAB dispatches to the CST version (which connects LTI models) instead of System Composer — it returns an empty 0×0 Connector silently.

The two-argument form connect(srcPort, dstPort) dispatches correctly via the port object's class method. Always use this form for explicit port-to-port connections.

For component-to-component auto-wiring by matching port names, the form connect(arch, [srcComp,...], [dstComp,...]) is also safe since arch dispatches correctly.


Port Multiplicity: Fan-Out Works, Fan-In Needs Separate Ports

A single output port can be connected to any number of input ports — just call connect(out, in1); connect(out, in2); .... This is clean 1→N fan-out; use it for broadcast buses (e.g. Supervisory.Schedule feeding every production unit).

A single input port cannot receive connections from multiple sources. SC is a structural modelling tool with no merge semantics — attempting to hook two outputs into one input either errors or silently fails. Give the destination component separate named input ports for each source (e.g. Status_Cook, Status_Pack, Status_Load) and wire 1:1. This pattern is verbose but unambiguous and matches how an MBSE diagram will render for review.


Composite Components: Mix ArchitecturePort and ComponentPort for Internal Wiring

When a component has sub-components (a composite), its boundary ports have two views:

ViewAccessed viaClassUsed for
Externalcomp.Ports / comp.getPort(name)ComponentPortConnections in the parent arch
Internalreturn value of addPort(comp.Architecture, ...)ArchitecturePortConnections inside the composite, to sub-component ports

A boundary "in" port acts as a source from inside the composite's sub-architecture (it delivers data to the sub-components), but its external ComponentPort still reports direction in. Using the wrong view throws:

Unable to connect ports because they have incompatible directions.

The exact rule (verified on R2025b)

Internal connections inside a composite require specific port classes on each side — verified empirically; mixing them wrong fails every time:

Connection inside the compositeSource portDestination portconnect call
Boundary → sub-componentSaved boundary ArchitecturePortSub-component ComponentPort (via getPort)connect(boundaryRef, sub.getPort("Name"))
Sub-component → boundarySub-component ComponentPort (via getPort)Saved boundary ArchitecturePortconnect(sub.getPort("Name"), boundaryRef)
Sub-component → sub-componentComponentPort (via getPort)ComponentPort (via getPort)connect(a.getPort("Out"), b.getPort("In")) — same as top-level

Two ArchitecturePorts on opposite sides of the boundary fail with "incompatible directions." That means the motorPow pattern — passing the return value of addPort(sub.Architecture, ...) to connect alongside a boundary ArchitecturePort — does not work. Use sub.getPort(name) instead for the sub-component side.

Pattern

matlab
function port = addTypedPort(comp, name, direction, iface)
    port = addPort(comp.Architecture, name, direction);
    port.setInterface(iface);
end

% Composite creation — keep the boundary refs
conv = addComponent(arch, "ConveyorSystem");
convPowerIn = addTypedPort(conv, "PowerIn", "in", ifPower);   % ArchitecturePort
convDiagOut = addTypedPort(conv, "DiagOut", "out", ifDiag);   % ArchitecturePort

% Sub-components — no need to save their port refs
motor = addComponent(conv.Architecture, "Motor");
addTypedPort(motor, "PowerIn",  "in",  ifPower);
addTypedPort(motor, "DiagOut", "out", ifDiag);

% Boundary -> sub: saved ArchitecturePort + sub's ComponentPort
connect(convPowerIn, motor.getPort("PowerIn"));        % ✓ correct

% Sub -> boundary: sub's ComponentPort + saved ArchitecturePort
connect(motor.getPort("DiagOut"), convDiagOut);        % ✓ correct

% DO NOT: two ArchitecturePorts across the boundary
% motorPow = addTypedPort(motor, "PowerIn", "in", ifPower);
% connect(convPowerIn, motorPow);                      % ✗ incompatible directions

External connections from the parent arch continue to use conv.getPort(name) (ComponentPort) as normal, the same as top-level connections between non-composite components.

Why addTypedPort(comp, ...) takes the Component (not its Architecture)

The helper called with a Component does addPort(comp.Architecture, ...) internally, so either calling convention works. Passing the Component is slightly cleaner for the common case and matches how non-composite components add ports. For a composite, the return value is still the internal ArchitecturePort — save it when you'll need it for boundary-to-sub wiring.


Variant Components

A variant component is a sibling concept to a composite: one wrapper with N alternative internal architectures (choices), only one of which is active at a time. Use this for architecture-options studies where you want to compare candidate topologies without duplicating the whole model file.

See code/buildMyVariantComposite.m for a working build template and code/tradeStudy.m for the driver that enumerates variants and emits a markdown comparison report.

Canonical build sequence

matlab
% 1. Build a regular composite with its boundary ports + the baseline content
comp = addComponent(arch, 'CookingLine');
addPort(comp.Architecture, 'RecipeData', 'in');
% ... add sub-components for the baseline ...

% 2. Convert to a variant wrapper. The baseline content becomes the first
%    auto-created choice (named the same as the wrapper).
vc = comp.makeVariant();

% 3. Re-fetch the wrapper -- the old `comp` reference is STALE (see below).
cookingLine = arch.getComponent('CookingLine');

% 4. Rename the auto-choice AND sync its variant condition to match.
ch = vc.getChoices();
ch(1).Name = 'V0_Baseline';
vc.setCondition(ch(1), 'V0_Baseline');

% 5. Add additional choices; each choice is a regular Component with its own
%    Architecture. Wire internal content inside each choice.
v1 = vc.addChoice({'V1_Parallel'});
addPort(v1.Architecture, 'RecipeData', 'in');
addComponent(v1.Architecture, 'ParallelA');
addComponent(v1.Architecture, 'ParallelB');

% 6. Choose the default active variant.
vc.setActiveChoice('V0_Baseline');

Gotcha — the original Component reference is stale after makeVariant

Once makeVariant returns, port lookups on the original variable fail with "not in same architecture scope". External connections placed before makeVariant survive the conversion (verified empirically), but any subsequent comp.getPort(...) or connect(...) calls must use a fresh reference:

matlab
vc = comp.makeVariant();
freshComp = arch.getComponent('CookingLine');   % returns VariantComponent

% Wrong -- stale scope:
connect(other.getPort('Out'), comp.getPort('RecipeData'));   % ✗

% Right:
connect(other.getPort('Out'), freshComp.getPort('RecipeData'));   % ✓

Gotcha — rename + setCondition must stay in sync

ch.Name = 'V0_Baseline' does not update the variant condition. setActiveChoice matches the given string against conditions, so without re-syncing:

matlab
ch(1).Name = 'V0_Baseline';
vc.setActiveChoice('V0_Baseline');
%   Error: Setting active choice for variant block '...' with variant control
%          'V0_Baseline' is not supported.

Fix by calling vc.setCondition(ch(1), 'V0_Baseline') immediately after the rename.

Gotcha — applyStereotype on the wrapper throws

Unable to apply stereotype 'Profile.Stereotype' on variant architecture '.../Wrapper'.

Apply the stereotype to each choice instead. Numeric property values set on the active choice propagate to the wrapper's instance at instantiate time, so the rollup callback sees the right value when it visits the wrapper. This is the mechanism that makes per-variant rollup estimates work.

Gotcha — string stereotype properties do NOT propagate choice → wrapper instance

Verified empirically on R2025b. A Type="double" property set on the active choice shows up on the wrapper's instance (instance.hasValue(...) == true, getValue(...) returns the choice's number). A Type="string" property shows up as hasValue == false on the wrapper's instance.

This matters when you want the rollup callback to branch on variant-specific behavior (e.g. MIN aggregation for serial, SUM for parallel). Encode the flag as a number, not a string:

matlab
% Works across the choice → wrapper instance boundary:
addProperty(st, "UseParallelThroughput", Type="double", DefaultValue="0");
setProperty(parallelChoice, stPath + ".UseParallelThroughput", "1");

% Does NOT work:
addProperty(st, "ThroughputAggregation", Type="string", DefaultValue='"MIN"');
setProperty(parallelChoice, stPath + ".ThroughputAggregation", '"SUM"');
%   -> wrapper's instance reports hasValue == false for this property

See mbse-architecture/references/analysis.md#topology-dependent-rollup for the rollup callback pattern that reads the numeric flag.

Gotcha — auto-layout skips inactive choices

comp.Architecture.Components on a VariantComponent returns only the active choice's sub-components, so a generic recursive arrangeComposites walker that follows .Architecture never lays out the inactive variants. Iterate vc.getChoices() explicitly:

matlab
function arrangeComposites(arch, pathPrefix)
    for comp = arch.Components
        subPath = [pathPrefix, '/', comp.Name];
        if isa(comp, 'systemcomposer.arch.VariantComponent')
            Simulink.BlockDiagram.arrangeSystem(subPath);
            for ch = comp.getChoices()
                choicePath = [subPath, '/', ch.Name];
                Simulink.BlockDiagram.arrangeSystem(choicePath);
                arrangeComposites(ch.Architecture, choicePath);
            end
        elseif ~isempty(comp.Architecture) && ~isempty(comp.Architecture.Components)
            Simulink.BlockDiagram.arrangeSystem(subPath);
            arrangeComposites(comp.Architecture, subPath);
        end
    end
end

Each choice's Simulink path is Model/Wrapper/ChoiceName — regular arrangeSystem works on them.

Minor — updatePortsFromChoices requires the 'Mode' kwarg

matlab
vc.updatePortsFromChoices();                    % error: 'Mode' must be specified
vc.updatePortsFromChoices('Mode','addPorts');   % ✓ adds missing wrapper ports from choices

Use this when choices define their own boundary ports and you want the wrapper to pick them up. If instead you add ports on the wrapper's Architecture before makeVariant and mirror them on each choice manually, you don't need this call.


Sequence Diagrams

A sequence diagram (SC calls these "Interactions") is a behavioral view layered on top of an architecture model. It shows how components collaborate over time for a specific scenario: which messages pass between them, in what order, under what guards. Unlike a Mermaid/PlantUML sequence diagram, every SC message is bound to real ports on real components — rename a port upstream and the sequence-diagram build errors, which is the feature not a bug.

See code/buildMySequenceDiagram.m for a working template.

Mental model

Model
 └── Interaction                      (one per scenario)
      ├── Lifelines                   (one per participant, bound to a Component)
      └── RootFragment
           └── Operands(1)            (pre-created default operand; messages live here)
                ├── addMessage(...)
                └── addFragment('Alt' | 'Loop' | 'Opt' | 'Par')
                     └── Operands(1..N) → more messages, recursively

Messages hang off operands, not directly off fragments or the interaction. Interaction.RootFragment.Operands(1) is where the straight-line message sequence goes. Alt creates a fragment with two operands; each has its own Guard string and its own addMessage.

Canonical build sequence

matlab
model = systemcomposer.openModel('MyModel');
destroyInteractionIfPresent(model, 'MyScenario');   % idempotent rebuild
diagram = model.addInteraction('MyScenario');

L1 = diagram.addLifeline('MyModel/CompA');          % path OR Component object
L2 = diagram.addLifeline('MyModel/CompB');

op = diagram.RootFragment.Operands(1);
op.addMessage(L1, 'OutPort', L2, 'InPort', 'request');
op.addMessage(L2, 'ReplyOut', L1, 'ReplyIn', 'reply');

save_system('MyModel');                              % interactions live INSIDE the .slx
open(diagram);                                       % show in Sequence Viewer

Gotcha — addMessage is 5 arguments, not 3

matlab
op.addMessage(L1, L2, 'request')                     % ✗ "Function requires 3 more input(s)"
op.addMessage('request', L1, L2, 1)                  % ✗ various complaints
op.addMessage(L1, 'OutPort', L2, 'InPort', 'request')% ✓ (src, srcPort, dst, dstPort, guard)

The 2nd and 4th args are port names that must exist on the components underlying the lifelines. If you pass a name that doesn't match a port on the source component, you get "Name must match a port on the component corresponding to the lifeline". This ties every message to the structural model and is the main reason to prefer programmatic SC sequence diagrams over free-form Mermaid.

Gotcha — messages do NOT live on Fragments directly

matlab
diagram.RootFragment.addMessage(...)                 % ✗ no such method
diagram.RootFragment.Operands(1).addMessage(...)     % ✓

Same for Alt/Loop/Opt fragments: get .Operands(i).addMessage(...). Alt.Operands has length 2 after addFragment('Alt'); set op1.Guard = "cond1" / op2.Guard = "cond2".

Gotcha — idempotent rebuild requires explicit destroy()

model.addInteraction(name) errors on duplicate name. Always delete the existing one first:

matlab
function destroyInteractionIfPresent(model, name)
    try, ixns = model.getInteractions(); catch, ixns = []; end
    for i = 1:numel(ixns)
        if strcmp(ixns(i).Name, name), ixns(i).destroy(); return; end
    end
end

Put the build step AFTER the architecture-model build step in buildAll — a rebuild of the architecture model wipes interactions along with everything else, so the sequence diagram must be re-created each time.

Guard syntax

Guards accept trigger names, boolean expressions in braces, or both:

'cookComplete'                           event name
'{Accepted==1}'                          boolean on interface fields
'rocketDocked{RocketPresent==1}'         event + condition
'rising(sw-1){sw==1}'                    signal transition + condition

Use interface-element names (from the dictionary — e.g., Accepted on QCVerdict) so the guard text stays consistent with the rest of the model.

Duration constraints

message.Start and message.End return MessageEvent objects that can be passed to addDurationConstraint:

matlab
t0 = msg1.End;
t1 = msg2.End;
diagram.addDurationConstraint(t0, t1, 't < 10sec');   % assertion on the render + runtime check

Persistence + viewing

  • Interactions are serialized inside the model's .slxsave_system(modelName) commits them; no separate file to track.
  • open(diagram) opens the SC Sequence Viewer canvas. diagram.open() also works.

Requirement traceability — see the slreq skill

slreq does not accept an Interaction object as a createLink source on R2025b, and the struct-based workaround (domain='linktype_sc_interaction') fails to persist when the containing .slx already hosts linktype_rmi_simulink Implement links. See simulink-requirements/SKILL.md — search for "Interaction"; it covers the recommended convention-based trace pattern.


Why dict.save() + Re-fetch Is Required

systemcomposer.createDictionary() creates the file but interface objects in memory aren't fully resolved until after dict.save(). If you call port.setInterface(iface) before saving, the model links the port to an unresolvable interface name — connections will silently fail and reopening the model shows "Unable to resolve interface" errors.

Pattern to always follow:

matlab
% 1. Add all value types and interfaces
thermalIface = addInterface(dict, "ThermalFluid");
addElement(thermalIface, "Temperature", Type="Temperature");

% 2. Save
dict.save();

% 3. Re-fetch — now safe to pass to setInterface
thermalIface = dict.getInterface("ThermalFluid");

Auto-layout

Always call Simulink.BlockDiagram.arrangeSystem before save(model) — programmatically added components all start at position (0,0) and stack on top of each other without it.

For hierarchical models, arrange every decomposed sub-architecture as well as the top level. Use the Simulink subsystem path modelName + "/ComponentName" for each level:

matlab
%% Layout and Save
Simulink.BlockDiagram.arrangeSystem(modelName + "/Powertrain");   % sub-levels first
Simulink.BlockDiagram.arrangeSystem(modelName + "/Drivetrain");
Simulink.BlockDiagram.arrangeSystem(modelName);                   % top level last
save(model);

Arrange sub-levels before the top level so the top-level layout has accurate size information for each component block. A sub-architecture that was never arranged remains a collapsed pile, and the top-level arrange won't fix it.


Re-run Safety for Profile Scripts

Calling applyProfile on a model that already has the profile throws a "uniqueness constraint" error. The cleanest solution is to rebuild the model from scratch at the top of the profile script — this guarantees a clean slate and makes both scripts independently idempotent. buildMySystemProfile already does this: its first call is always buildMySystemModel(...).


Multi-Domain Interface Patterns

Use Type="double" for all elements and document physical units in comments. Do not use addValueType for physical quantities — it creates Simulink.ValueType objects the bus compiler cannot resolve (breaks "update diagram").

See code/addCommonInterfaces.m for an illustrative starting point covering Thermal, Electrical, Mechanical, and UserCommand interfaces:

ifaces = addCommonInterfaces(dict)

Returns a struct (ifaces.ThermalFluid, ifaces.ElectricalPower, etc.). Remember to call dict.save() and re-fetch interfaces before passing them to setInterface() — see the re-fetch pattern above.


Architecture Views — filtered lenses on a large model

Once a model grows beyond a couple of dozen components, navigating it becomes a drag. System Composer's view architectures are named, saved lenses that filter the architecture by a stereotype-property query. A view appears in the model canvas dropdown and in the Views Gallery (openViews(model)), and matching components glow in the color you gave the view. This is how Gulfstream's eSAM method routinely surfaces cost drivers, high-power components, per-supplier subsets, etc. without hand-drawn diagrams that rot.

Two mechanisms:

Query-driven views — a single stereotype-property constraint picks members automatically:

matlab
import systemcomposer.query.*;
q = PropertyValue("MyProfile.ComponentProperties.Cost_credits") > 150000;
v = createView(model, "CostDrivers", Select=q, Color="#D62728");

Color gotcha: Color accepts hex strings universally ("#D62728") and some but not all named colors — "red" and "blue" work, "magenta" errors with "The value of 'Color' is invalid. The color must be a hex color or RGB value." Prefer hex.

PropertyValue(path) returns an object that overloads >, <, >=, <=, ==, ~= to build a query constraint — so the pv > 150000 expression builds a systemcomposer.query.Compare object. Pass it as Select=. The view refreshes automatically as stereotype properties change.

To check ad-hoc what a query matches without committing to a view:

matlab
matches = find(model, q);   % returns a cell array of qualified-name STRINGS,
                            % not Component objects (easy mistake)
for i = 1:numel(matches), disp(string(matches{i})); end

Explicit-element views — for anything a single property query can't express (allocation-driven groupings, hand-picked subsets, per-supplier partitions), create an empty view and add elements by hand:

matlab
v = createView(model, "ControlRealization", Color="blue");
v.Root.addElement(arch.getComponent('ControlCabinet'));
for sub = arch.getComponent('ControlCabinet').Architecture.Components
    v.Root.addElement(sub);
end

Both mechanisms are additive — you can start with a query and then addElement to include extras that didn't match.

Idempotency when rebuilding

Views are saved inside the .slx. Our build scripts recreate the .slx from scratch on every run, which wipes views along with everything else. So a view-creation script has to run after the relevant buildXxx.m and be idempotent itself:

matlab
try, deleteView(model, "CostDrivers"); end %#ok<TRYNC>   % guard against first-run-missing
v = createView(model, "CostDrivers", Select=q, Color="red");

See code/buildMyViews.m for a parameterised helper that takes a list of view specs and creates them all.

find() returns strings, not components

A recurring mistake: find(model, constraint) returns a cell of qualified-name strings like "MyModel/Parent/Sub", not systemcomposer.arch.Component objects. If you need the component object to, e.g., call getPropertyValue, use model.lookup("Path", pathString) to resolve it.


Verifying Your Model

Run these checks after every build. They catch real problems that the build step itself won't flag.

1. Check for unconnected ports

Ports that were added but never wired are silently valid at build time but represent incomplete or inconsistent architecture. See code/checkUnconnectedPorts.m:

checkUnconnectedPorts(modelName)

2. Update diagram

set_param update catches any remaining type resolution issues (e.g. a bad interface element type that slipped through):

matlab
set_param("MySystem", "SimulationCommand", "update");

Expected warning for pure architecture models: Architecture model contains no components or all components are virtual. This is normal — architecture components have no simulation behaviour. It does not indicate a problem with your model.

If you see a type resolution error instead (e.g. DataType 'X' did not resolve), the cause is almost always an addElement call using a value type name instead of a Simulink base type — see the gotchas table above.

3. Check stereotype properties (if using profiles)

matlab
comp  = arch.getComponent("ComponentA");
props = comp.getStereotypeProperties();
for i = 1:numel(props)
    fprintf("  %s = %s\n", props(i), comp.getPropertyValue(props(i)));
end

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the System Composer AI skill do?

Use this skill when authoring reusable, idempotent MATLAB scripts that build System Composer architecture models via the architecture-modeling API — `systemcomposer.createModel`, `addComponent`, `addPort`, `setInterface`, `connect(srcPort, dstPort)`, interface dictionaries (.sldd) with `addInterface`/`addElement`, profiles/stereotypes with `Profile.createProfile` and `addStereotype`, or `systemcomposer.allocation.createAllocationSet`. Also trigger when debugging these APIs (connections that don't appear, interfaces that don't resolve, profile save errors, `createAllocationSet` signature-mis...

Why use System Composer on TypingMind?

Because you install it once and use it with any model. System Composer is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install System Composer in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/matlab/agent-skills-playground/tree/main/demos/mbse-with-agentic-ai/skills/system-composer. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use System Composer?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with System Composer?

As many as you like. As long as a model supports skills, you can use System Composer with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the System Composer AI skill free?

It is published on GitHub by matlab. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇