Appearance
Scripting API (window.CivilKit)
CivilKit Studio exposes a small, stable scripting surface on window.CivilKit. It lets you drive the tool from code: load a model, solve it, read the results, and export IFC, without depending on Studio's internals.
This is a v1 in-browser scripting API
window.CivilKit is an in-browser API. It runs where Studio is loaded - the browser devtools console, or a page that embeds Studio. The solver and the whole API run client-side.
There is now a REST endpoint and an MCP server, and this paragraph used to say there was not. Both drive the same Studio through the same public API - see the MCP guide - and both advertise their whole tool table in one call (tools/list, GET /v1/tools, or the unauthenticated GET /openapi.json). What is true is that window.CivilKit itself is in-browser: it does not reach a server, and the solver runs client-side either way.
It is a v1 surface: a curated subset of what Studio can do, intended for the common "load -> solve -> read/export" loop. For the full headless solver (raw JSON model in, JSON results out) see the WASM API Reference.
Quick start
Open Studio, then in the browser console:
js
await CivilKit.loadSample('prattTruss');
await CivilKit.solve();
const ifc = CivilKit.exportModelIfc();
console.log(ifc.slice(0, 200));CivilKit is a frozen object. Every method throws a clear Error (prefixed CivilKit:) if Studio is not ready yet or the backing feature is missing in the current build - check CivilKit.isReady() first if you are scripting against a freshly-loaded page.
Reference
The methods below are grouped: Model, Query, Edit, Solve & Results, Design, Diagnostics, Export, Events.
CivilKit.version
A version string for the scripting surface itself (currently '1.2'). This is the API contract version, not the app or solver version.
js
CivilKit.version // '1.2'CivilKit.isReady()
Returns true once the backing internals exist (Studio has booted and the solver module loaded). Never throws. Use it to gate scripts that run right after page load.
js
if (CivilKit.isReady()) { /* safe to call the rest */ }CivilKit.getModel()
Returns the current model as a plain, serialisable object (a clone - mutating it does not affect Studio). Same shape as the Model JSON schema.
js
const model = CivilKit.getModel();
model.members.length;CivilKit.loadModel(model)
Loads a model object into Studio (replacing the current model). Accepts the same shape getModel() returns. Throws if model is not an object.
js
const m = CivilKit.getModel();
m.name = 'Edited copy';
CivilKit.loadModel(m);CivilKit.loadSample(key)
Loads a built-in sample by its gallery key. Throws on a missing/empty key.
js
CivilKit.loadSample('portalFrame');Common keys: prattTruss, portalFrame, multiStorey, spaceFrame, cantilever, simplySupported, continuousBeam2, floorSlab, steelPortalReal. (The full list is the Studio sample gallery.)
CivilKit.validate()
Runs the model health check and returns an array of issue objects (each with a code and context). An empty array means no problems were found. Returns a clone.
js
const issues = CivilKit.validate();
if (issues.length) console.warn('Model has issues:', issues);Query
Read-only access to the model collections. Every method returns a clone, so mutating the result never touches Studio's live state.
CivilKit.getNodes() / getMembers() / getSections()
Return cloned arrays of the model's nodes, members, and sections. Each returns [] if the collection is absent.
js
CivilKit.getNodes().length;
CivilKit.getMembers().map(m => m.id);CivilKit.getNode(id) / getMember(id)
Return a single node/member by its stable string id (not the engineer num), or null if not found.
js
const n = CivilKit.getNode('n1'); // { id, num, x, y, z, restraint_flags }
const m = CivilKit.getMember('m1'); // { id, num, node_a, node_b, section_id, ... }CivilKit.memberRoles()
Every member's structural role, derived from the geometry the way the shed verbs derive it: column, rafter, purlin, girt, endGirt, eaveStrut, ridgeStrut, mullion, prop, brace, strut. Returns [{ id, num, role }], with role: null for a member the derivation does not recognise.
This is the same derivation add_member_load selects by, so it is how you see what a role selector will actually hit before you apply a load to it. Returns [] on a model with no nodes.
js
const purlins = CivilKit.memberRoles().filter(r => r.role === 'purlin');CivilKit.getRevision()
A signature of the current model, as a string. It changes when the model changes and not otherwise, so it is what to compare when you want to know whether an edit landed - or whether the model moved under you between two reads. It says nothing about what changed.
js
const before = CivilKit.getRevision();
CivilKit.addNode({ x: 0, y: 0, z: 3 });
before === CivilKit.getRevision(); // falseCivilKit.listSections(opts?)
Search the AU steel catalogue. opts.family takes either the short code or the full category name - UB, UC, PFC, SHS, RHS, CHS, EA, UA, FL, ROD, C, Z - and opts.match is a case-insensitive substring of the name. Both are optional; with neither, you get the whole catalogue.
Each row is the shortlist an engineer picks from, not the full property set: { name, category, massPerMetre_kgm, depth_mm, flangeWidth_mm }.
js
await CivilKit.listSections({ family: 'UB', match: '360' });CivilKit.sectionProps(name)
The full property set for one catalogue section by name (case-insensitive): geometry and analysis properties together. Throws if the name is not in the catalogue rather than returning a default - a silent fallback here would design steel nobody specified. Use listSections to search.
js
const p = await CivilKit.sectionProps('360UB50.7');Edit
Programmatic model editing. These mutate the model the same way the interactive tools do (new id/num, identical object shape), rebuild the scene, push an undo step, and fire the modelChanged event. Editing invalidates the current results (you must re-solve).
CivilKit.addNode({ x, y, z, restraint? })
Adds a node at {x, y, z} (metres). restraint is an optional 6-bit restraint_flags integer or a preset name, 'free', 'pinned', 'roller' or 'fixed' (default 0 = free); any other string is refused. Returns the created node.
js
const n = CivilKit.addNode({ x: 0, y: 0, z: 0, restraint: 'pinned' }); // pinned base
const m = CivilKit.addNode({ x: 6, y: 0, z: 0, restraint: 63 }); // fixed, by flagsCivilKit.newModel(name?)
Loads an empty schema v2 (Z-up) document and returns it, discarding whatever was open. The honest start for a script building from nothing: no nodes, no members, no assumed load cases.
js
CivilKit.newModel('Warehouse A');CivilKit.addMember({ nodeA, nodeB, section?, material?, memberType? })
Adds a member between two existing node ids. nodeA and nodeB are required (and must differ); section/material default to the model's first of each, memberType defaults to 0 (beam). Returns the created member. Throws if a node id is missing or unknown.
js
const m = CivilKit.addMember({ nodeA: n1.id, nodeB: n2.id });CivilKit.setSupport(nodeId, preset)
Sets a node's support. preset is a name - 'free' (0), 'pinned' (31), 'roller' (30), 'fixed' (63) - or a raw restraint_flags integer. Returns the updated node.
js
CivilKit.setSupport('n1', 'fixed');
CivilKit.setSupport('n2', 31); // raw flags also acceptedCivilKit.assignSection(members, name, country?)
Assigns a real catalogue section (by name, e.g. '310UC158') to members - all of them (null), an array of ids, or a single id. The members take the true profile geometry and name, not a placeholder. country is the catalogue code (default 'au'). Returns { assigned, section }. Async - it loads the catalogue.
js
await CivilKit.assignSection(null, '310UC158'); // every member
await CivilKit.assignSection(['m1', 'm2'], '610UB101'); // just these twoCivilKit.meshAll(opt?)
Meshes every detected panel (a bay bounded by four members) into a compatible grid of plate elements in one call - the scripted form of the Mesh all tool. opt: { elementSize?, plane?, thickness?, materialId? }. Returns a summary of what was meshed.
js
CivilKit.meshAll({ plane: 'floor', elementSize: 3 });CivilKit.setPlateOrthotropy(plates, spec, opt?)
Makes plates orthotropic - CLT floors and walls, profiled decking, hollowcore - or isotropic again. plates is a plate id or number, an array of them, or null for every plate. spec is one of:
- a CLT preset name:
'clt-3s-105','clt-5s-175'or'clt-7s-245'(generic 35 mm C24 layups; check them against the manufacturer). A preset also sets the plates' thickness to the layup's. - a full spec
{ axis, input }, whereinputis{ kind: 'material', e1, e2, g12, nu12, g13, g23 }(Pa) or{ kind: 'rigidities', a11, a22, a12, a66, d11, d22, d12, d66, s13, s23 }(per metre width: A and S in N/m, D in N·m). null, to make the plates isotropic again.
axis (or opt.axis for a preset, default [1, 0, 0]) is the strong direction (the grain of the outer layers, or the ribs) as a GLOBAL vector; the solver projects it onto each plate, and refuses one within 1° of a plate's normal. A plate that is not in the model is refused by name. Returns { set, orthotropic, thickness }.
js
CivilKit.setPlateOrthotropy(null, 'clt-5s-175', { axis: [0, 1, 0] }); // every plate, grain along Y
CivilKit.setPlateOrthotropy([12, 13], null); // back to isotropicOn an orthotropic plate the buckling factor is not computed and von Mises is not a strength check; stresses and moments come back in each element's local axes. The solve says so in its warnings.
Solve & Results
CivilKit.setActiveCase(ref) / getActiveCase()
The case or combination every result reader answers for: getReactions, getDisplacements, getMemberUtilisations, the contour and the diagrams all report the active one. ref is an id or a name, and either a load case or a load combination.
setActiveCase throws if nothing matches, and the message lists the cases and combinations that do exist - a silent no-op here would leave every subsequent read answering for the wrong case with nothing to say so.
getActiveCase() returns { id, name, kind } where kind is 'case' or 'combination', or null when nothing is selected.
js
CivilKit.setActiveCase('1.2G+1.5Q');
CivilKit.getActiveCase(); // { id: 4, name: '1.2G+1.5Q', kind: 'combination' }CivilKit.solve()
Runs the analysis using the currently-selected analysis type and load cases (the same path as the Solve button). Returns a Promise that resolves to a compact results summary (see getResultsSummary()). Use getResults() for the full object.
js
const summary = await CivilKit.solve();
// { analType: 'static', loadCases: 1, displacements: 8, reactions: 2, memberCapacities: 12 }CivilKit.getResults()
Returns the full current results object (a clone), or null if nothing has been solved yet. Shape matches the Results JSON schema.
js
const r = CivilKit.getResults();
r?.load_cases[0].displacements;CivilKit.getResultsSummary()
A compact summary of the current results, or null if nothing has been solved:
js
CivilKit.getResultsSummary();
// { analType, loadCases, displacements, reactions, memberCapacities }CivilKit.getMemberUtilisations()
Per-member governing utilisation after a solve - the same numbers as the design table:
js
CivilKit.getMemberUtilisations();
// [{ memberId, gov, govBy, status: 'OK' | 'OVER', code: 'AS4100' | 'AS4600' }, ...]gov is the governing utilisation ratio (1.0 = at capacity); govBy names the governing clause/term. Returns null if nothing has been solved (or the build has no design check).
Each row also carries capacitySource: 'case' when the utilisation was paired with the capacity row the active case produced itself (its own alpha_m), 'union' when the row came from another case, which is what a combination or the envelope gets, since they carry no capacity set of their own.
CivilKit.getDesignCoverage()
After a solve: is every member accounted for by the design pass? Returns { members, checked, deactivated, unchecked, unaccounted } or null before a solve. deactivated is [{ caseId, name, ids }], the slack tension/compression- only members per case (zero force, no capacity row there); unchecked is [{ reason, ids, text }], members the pass skipped and named (unresolved_section, unresolved_material, missing_dimensions, cold_formed_declined, monosymmetric); unaccounted lists any member that is none of the three and should be empty. A non-empty unaccounted is a defect in the engine, not the model.
js
const cov = CivilKit.getDesignCoverage();
if (cov.unaccounted.length) throw new Error('members fell through the design pass: ' + cov.unaccounted);CivilKit.getReactions()
Support reactions for the active load case, or null if nothing has been solved. Each entry: { node_id, rx, ry, rz, mx, my, mz } (forces in N, moments in N·m).
js
const r = CivilKit.getReactions();
const totalVertical = r.reduce((s, x) => s + x.ry, 0);CivilKit.getDisplacements()
Nodal displacements for the active load case, or null if nothing has been solved. Each entry: { node_id, ux, uy, uz, rx, ry, rz } (translations in m, rotations in rad).
js
const d = CivilKit.getDisplacements();
const maxSway = Math.max(...d.map(x => Math.abs(x.ux)));Code loads & design workflow
Turn the built-in AS/NZS load engines into named load cases, generate the code combinations, and produce the report - the scripted equivalent of the load generators and the Report button. These wrap the same Rust engines the UI uses.
CivilKit.windLoads(opts?)
Applies AS/NZS 1170.2 wind as a lateral load case: computes the site design pressure for the building height, then distributes net along-wind storey forces onto the windward-face nodes. opts: { region?, terrain?, direction?, netCp?, returnPeriodFactor?, caseName? } (defaults: region 'A', terrain '3', direction 'x', netCp 1.3, caseName 'Wind'). The created case is tagged load-type Wind (Wu) so combinations pick it up. Returns the derived pressure and total force.
js
CivilKit.windLoads({ region: 'A', terrain: '3', direction: 'x' });
// { q_Pa: 1167, vDes_ms: 44.1, totalForce_kN: 1261, storeys: 10, caseName: 'Wind' }CivilKit.openStructureWind(opts)
AS/NZS 1170.2 Appendix B and Clause 5.4.4, routed by kind: 'hoarding' (or 'wall'), 'free_roof', 'attached_canopy' (or 'carport'), 'local_pressure'. Site defaults come from the project's Design criteria. Returns the engine's result: q_pa at the reference height, the net pressure coefficients (both alternatives where the table prints two), c_shp, pressures in Pa, steps, warnings and tables. Refusals throw with the standard's reason. Does not apply loads to the model. See Open structure wind.
js
CivilKit.openStructureWind({ kind: 'free_roof', roof: 'pitched', alpha_deg: 22.5, h: 5, d: 10, b: 20, blockage: 0.5, wind: '0' });
// { standard: 'AS/NZS 1170.2:2021 Appendix B.3 (Table B.5)', q_pa: 1013, zones: [...], design_cases: [[-0.6, -0.4], ...], ... }CivilKit.floorVibration(opts?)
SCI P354 footfall response (the general method) on the modal results on screen; throws without a modal solve. opts: { excitationNode?, responseNode?, unitShape?, damping? (ratio or a Table 4.1 name), weighting? ('Wb' | 'Wg' | 'Wd'), floor? ('general' | 'enclosed' | 'staircase'), paceMin?, paceMax?, walkingPath?, limit? }. Returns response_factor, governing, governing_pace_hz, floor_class, the steady_state and transient breakdowns with their sweeps, and warnings. See Floor vibration.
js
CivilKit.floorVibration({ damping: 'furnished', limit: 8 });
// { response_factor: 3.2, governing: 'steady-state (resonant)', governing_pace_hz: 2.0, ok: true, ... }CivilKit.craneLoads(opts)
EN 1991-3 Section 2 crane actions from the supplier's data sheet. opts: { bridge_weight_kn, crab_weight_kn, hoist_load_kn, span_m, min_approach_m, wheels_per_rail, wheel_spacings_m, hoisting_class, hoisting_speed_ms, driven_wheels, friction, phi_5, guide_spacing_m, skew_angle_rad, travel_speed_ms, buffer_stiffness_kn_m, fatigue_class }. Returns the dynamic factors, the characteristic wheel loads, the Table 2.2 load groups, the drive force and its transverse couple, skewing, buffer forces, the fatigue damage-equivalent load, and axle_weights_kn / axle_spacings_m for the runway envelope. Unverified (draft text); AS 1418 is not carried. See Crane loads.
js
CivilKit.craneLoads({ bridge_weight_kn: 120, crab_weight_kn: 20, hoist_load_kn: 100, span_m: 20, min_approach_m: 1 });
// { q_r_max_kn: 97.3, groups: [...], axle_weights_kn: [97.3, 97.3], axle_spacings_m: [3], verified: false, ... }CivilKit.robustnessTies(opts?)
EN 1991-1-7 Annex A robustness: the consequences class, the strategy it prescribes, and the tie forces. opts: { class: '1' | '2a' | '2b' | '3', construction: 'framed' | 'wall', gk_kpa, qk_kpa, psi, tie_spacing_m, tie_span_m, storeys, z_m, column_storey_reaction_kn, wall_clear_height_m, wall_thickness_m, wall_plan_area_mm2 }. Returns t_internal, t_perimeter, f_t, t_vertical, the 15 % notional-removal limit and the 34 kPa key-element action. Unverified against the published edition. See Robustness.
js
CivilKit.robustnessTies({ class: '2b', gk_kpa: 3, qk_kpa: 5, tie_spacing_m: 2.5, tie_span_m: 6 });
// { t_internal: 96, t_perimeter: 75, key_element_ad_kpa: 34, verified: false, ... }CivilKit.embodiedCarbon(opts?)
IStructE embodied carbon (A1 to A3) of the model's take-off: mass by material class times the How to calculate embodied carbon Table 2.3 factors. opts: { preset: 'uk' | 'global', factors, rebarKgPerM3, floorArea_m2 }. Returns the per-class table, the total in kgCO2e and tCO2e, and per m² GIA with the guide's sense check when a floor area is given. Pure JS; no solve needed. See Embodied carbon.
js
CivilKit.embodiedCarbon({ preset: 'uk', floorArea_m2: 1200 });
// { total_tco2e: 41.2, per_m2: 34.3, byClass: [...], senseCheck: { verdict: 'below ...' } }CivilKit.seismicLoads(opts?)
Applies AS 1170.4 earthquake as a lateral load case: computes the base shear from the model's self-weight and the site, then distributes it up the storeys (Fx proportional to w·hᵏ, AS 1170.4 Cl 6.3) through the same storey distribution the Studio's Load generator applies, so the API and the dialog write one pattern. opts: { zone?, soil?, ru?, sp?, period?, direction?, caseName? } - period is estimated from the height if omitted. The case is tagged load-type Seismic (Eu). Returns the base shear and storey forces.
js
CivilKit.seismicLoads({ zone: '0.08', soil: 'De' });
// { baseShear_kN: 9471.2, weight_kN: 55335.4, period_T: 1.4, k: 1.45, storeyForces: [{ z, F_kN }, ...] }CivilKit.snowLoads(opts)
Applies AS/NZS 1170.3 snow as a downward UDL on the roof members you name. opts: { memberIds, tributaryWidth, region?, altitude?, exposure?, terrainFactor?, pitch?, pitch2?, annualProbability?, caseName? }. memberIds and tributaryWidth (metres) are REQUIRED and are not guessed: a snow load is a pressure, and turning one into a member UDL needs the width of the member carrying it, while which members are roof is a question only the model's author can answer. Everything else defaults to the project's Design criteria, and the annual probability to AS/NZS 1170.0 Table F2's snow column - a different column from wind and earthquake at the same importance level.
Returns the ground snow load, every load case the standard asks for with its figure number, the edge overhang load (Cl 4.2.3) and the UDL applied. The case is tagged load-type Snow (Su), so loadCombinations() factors it as snow.
A site the standard does not cover is refused by name rather than extrapolated, and a named Table 5.2 site is refused with what to do instead: that table is not carried, because transcribing ground snow loads out of a scan would put invented numbers into designs.
js
CivilKit.snowLoads({ memberIds: rafters, tributaryWidth: 3, pitch: 15 });
// { groundSnow_kPa: 2.4, peak_kPa: 1.68, ce: 1, kp: 1.5, cases: [...], udl_kNm: 5.04, caseName: 'Snow' }CivilKit.partForce(opts)
AS 1170.4:2024 Section 8 - the earthquake force on a non-structural part or component: a parapet, a rooftop plant item, a storage rack, a curtain wall.
opts: { weight_kN, method?, hx?, hn?, floorAcceleration?, importance?, mounting?, ductility?, mechanicalOrElectrical?, location?, z?, soil?, annualProbability? }. Site values default to the project's Design criteria.
method picks the clause:
'simple'(the default, Cl 8.3) needshn, the structure's height, andhx, the height the component is attached at. It needs no analysis of the structure at all, which is why most parts are designed by it.'acceleration'(Cl 8.2(1)) takesfloorAcceleration, the effective floor acceleration at the component's level as a fraction of g. Cl 8.2 floors that atkp Z Ch(0)and the result tells you (accelerationFloored) when it did - a floor acceleration below the ground acceleration is the one slip in this clause that is silently unconservative.
The three factors are the closed sets the clause prints, not free numbers: importance ('other', 'life-safety-critical', 'importance-level-4'), mounting ('other', 'flexible-spring-mechanical') and ductility ('rigid', 'non-ductile', 'flexible-ductile', 'connection').
It does not touch the model. A part is not a member of the analysis, so there is no element to hang a load on; applying the force somewhere would mean guessing which member carries it. You get the force and its provenance, to size a fixing with.
bound says whether Cl 8.2(1)'s 0.05 W_c floor or 0.5 W_c cap decided the answer, with unbounded_kN beside it. Set mechanicalOrElectrical and vertical_kN carries Cl 8.1.3's vertical force - half the horizontal one, and an alternative load case to it, never an addition.
js
// A 6 kN parapet at the top of a 30 m frame.
CivilKit.partForce({ weight_kN: 6, hx: 30, hn: 30 });
// { force_kN: 1.872, bound: 'computed', acceleration: 0.104, heightAmplification: 3,
// importanceFactor: 1, amplificationFactor: 1, ductilityFactor: 1,
// vertical_kN: null, clause: 'AS 1170.4:2024 Cl 8.3' }
// Spring-mounted plant on a floor whose acceleration came from the analysis.
CivilKit.partForce({ weight_kN: 20, method: 'acceleration', floorAcceleration: 0.3,
importance: 'life-safety-critical', mounting: 'flexible-spring-mechanical',
ductility: 'flexible-ductile', mechanicalOrElectrical: true });
// { force_kN: 6, vertical_kN: 3, clause: 'AS 1170.4:2024 Cl 8.2(1)', ... }CivilKit.loadCombinations(opts?)
Generates the code load combinations from each load case's type (Dead / Live / Wind / Seismic ...) and stores them on the model. opts: { code? } - 'asnzs' (default), 'asce7' or 'eurocode'. Set the case types first (windLoads / seismicLoads tag their own; give Dead/Live cases a load_case_type). Returns the count and combination names.
js
CivilKit.loadCombinations();
// { count: 7, code: 'asnzs', combinations: ['1.35G', '1.2G+1.5Q', '1.2G+Wu+psiCQ', '0.9G+Wu', 'G+Eu+psiCQ', ...] }CivilKit.report()
Builds the calculation report as an HTML string - the same content as File → Report (title block, load cases and combinations, member design, results). Solve first so the results sections populate.
js
const html = CivilKit.report(); // e.g. save, print, or open in a new tabCalc sheet
The calculation sheet (the Calc tab) is a typed document, and the text is the source of truth: the API writes sheet text, never blocks, so an API-written line is indistinguishable from a typed one. Grammar: prose lines; name: expr defines a variable; {domain:id.field} references a live model value (see calcFields()); a trailing | note is the clause column.
CivilKit.calcGet()
Returns the sheet as { sheet, engineBlocks } (a clone; sheet is '' on a fresh model). engineBlocks are engine-authored working appended by calcInsertWorking() - they live beside the text, not inside it.
js
const { sheet } = CivilKit.calcGet();CivilKit.calcSet(text) / calcAppend(text)
Replace (or append to) the whole sheet in one call. The derived block cache is re-derived and the model is marked dirty + persisted. Both return { lines, blocks } counts.
js
CivilKit.calcSet('q: 5\nM: q * 6^2 / 8');
CivilKit.calcAppend('V: q * 6 / 2'); // no get-then-set raceCivilKit.calcResolve()
The machine check-back: resolves the sheet against the current model + results and returns one entry per block - { line, kind, label, expr, value, unit, unresolved, input } - so a script can assert on the numbers a stamped sheet would show. A block that cannot resolve carries its unresolved reason, never a stale value and never zero.
js
const blocks = CivilKit.calcResolve();
blocks.find(b => b.label === 'M').value; // 22.5CivilKit.calcFields()
The valid-reference catalogue: every live model value the sheet may point at, as { label, detail, insert } rows - the same list the editor's picker offers. insert is the literal {token} text to paste into sheet text.
js
CivilKit.calcFields();
// [{ label: 'F1.q_max', detail: 'kPa · checked', insert: '{footing:n7.q_max@env}' }, ...]CivilKit.calcInsertWorking(kind)
Appends engine working - the scripted form of the + CivilKit working / + Footing working buttons. kind is 'member' (code check for the governing member) or 'footings' (AS 3600 working per footing type). Returns { inserted }; 0 means nothing to insert yet (solve / design footings first).
js
await CivilKit.solve();
CivilKit.calcInsertWorking('member');CivilKit.calcRemoveWorking(which?)
Remove engine-working blocks from the sheet - 'all' (the default), or a kind. Returns how many were removed.
CivilKit.calcImport(bytes, opts?)
Import an .xlsx onto the sheet and check it against itself. Every formula is translated into the sheet's grammar in the workbook's own row labels, evaluated here, and compared with the value the workbook cached - so a disagreement is either a stale workbook or a defect in the import, and both are worth knowing.
js
const r = await CivilKit.calcImport(bytes, { name: 'beam.xlsx' });
console.log(r.counts); // { calc, input, prose, data, refusal, unitUnread }
r.check.rows.filter(x => !x.agrees).forEach(x => console.log(x.label, x.cached, 'vs', x.ours));
await CivilKit.calcImport(bytes, { insert: true }); // now write it to the sheetbytes is an ArrayBuffer, a typed array or a byte array. Nothing is written unless insert is true: an import of this kind is partial by design - some rows become calculations, some data, some refuse, some are waiting for a unit - and a caller that has not read the summary should not be committing it to a document somebody signs.
A formula the sheet cannot evaluate arrives as a refusal naming its cell, never as the number the workbook cached. A unit the sheet does not know (kNm, cm3 are common spreadsheet shorthand) is marked rather than guessed at.
CivilKit.calcHtml()
The sheet as a standalone printable HTML string (title header + print rendering) - the same document the Calc tab's PDF button prints. Returns null when the sheet is empty.
js
const html = CivilKit.calcHtml(); // save it, or print itDesign
CivilKit.autoDesignConnections()
Auto-designs connections over the active model and load case (AS 4100), stores them on the model, and returns the generated connection objects.
js
await CivilKit.solve(); // connections need solved forces
const conns = CivilKit.autoDesignConnections();CivilKit.getConnection(id) / connections()
connections() is the schedule - one row per joint with its class, topology code, template, component-method verdict, governing utilisation and FEA state. getConnection(id) is one joint in full: geometry in SI (metres, pascals), each member with the frame force beside the AS 4100 Cl 9.1.4-floored design action, every component check with its clause, and the joint-FEA summary if one has been run.
CivilKit.solveConnection(id)
Re-check one joint by the component method (AS 4100 closed form). Returns its status, governing utilisation and every check.
CivilKit.setConnectionParam(id, kind, index, key, value)
Set one geometry value in SI: kind is 'plates' | 'bolts' | 'welds' | 'concrete', e.g. setConnectionParam('C1', 'plates', 0, 't', 0.025) for a 25 mm plate. Marks the joint manual - so a later auto-design leaves that value alone and records what it would have done in the Proposals tab - and stales its solve.
CivilKit.applyConnectionTemplate(id, template)
Apply a library template: fin_plate, web_cleat, flex_end_plate, end_plate, ext_end_plate, base_plate, apex_plate, splice, gusset and the rest.
CivilKit.applyConnectionToSimilar(id)
Copy this joint's template and every sizing field onto the similar joints - same class, same supported section - respecting locks. Returns { n, skipped }.
CivilKit.connectionPerCase(id)
Every load case and combination checked on the joint as it stands: per-row utilisation, status, governing check, the forces used beside the frame's own, and which components the Cl 9.1.4 floor governs. The governing row is marked.
CivilKit.runJointFea(id, quality)
Run the 3D shell joint FEA (linear elastic, contact under the base plate, bolts as connectors). quality is 'coarse' | 'normal' | 'fine'. Async; seconds to tens of seconds. Returns convergence, mesh size, peak von Mises and bolt forces.
Footings
CivilKit.designFootings() / footings()
designFootings() sizes a footing at every support from the solved reactions; footings() returns the schedule without redesigning. Rows are typed: mark, size, count and a per-footing state - pass, grew, partial, fail, none or manual.
CivilKit.getFooting(idOrNode)
One footing in full: geometry, reinforcement and every check with its result.
CivilKit.footingDemand(nodeId)
The governing pad-footing demand at a support node, across every solved case and combination: { nStar, mxStar, myStar, vStar } in SI. What you would hand a footing calculation, without running the footing design.
Extensions
Studio's .ckext modules, driven without a DOM. This is the loop for an agent that wrote a module and needs to fix it.
CivilKit.extensionRun(bytes, opts?)
Installs a .ckext bundle - bytes is a Uint8Array or a plain array of its zip bytes - and exercises it headlessly: its worked examples with a per-row diff, the form build_ui produces, one check run, and the capabilities it actually touched. opts.inputs overrides the example inputs.
The badge it reports is the same test gate the module manager shows, so a module that passes here is a module that passes there.
js
const report = await CivilKit.extensionRun(bytes);
report.badge; // the gate the module manager will showCivilKit.extensionList()
The installed extensions: [{ id, name, version, badge, contributes, ... }].
Export
CivilKit.createConnectionsAtNodes(nodeIds)
Build the joint at every node in one call, through the same generator the auto-design pass uses. This is the scripting form of the model view's bulk bar action (⬡ Create connections (N)), which appears when a multi-selection contains nodes.
js
const ids = CivilKit.State.model.nodes.slice(0, 40).map((n) => n.id);
const { created, existing, refused } = CivilKit.createConnectionsAtNodes(ids);
created.length; // how many joints were built
existing; // nodes that already carried one, left untouched
refused; // [{ nodeId, reason }] - a node with no member, or one the classifier finds no joint atA second joint at a node that already has one would be two records of one physical connection, so those are left alone and counted in existing. Nothing is dropped in silence: every node that produced no joint is in refused with its reason. No solve is required - the load case is passed through including null, so joints built on an unsolved model are sectioned from their members and carry no demand until it is solved.
CivilKit.exportConnectionDxf(id) / exportConnectionIfc(id)
One joint as a 2D fabrication drawing (DXF) or as IFC. Returns the file's text.
CivilKit.exportConnectionDstv(id)
The DSTV / NC1 files for one joint's fabricated plates, for the punching, drilling or plasma machine. Returns { files, skipped }:
js
const { files, skipped } = CivilKit.exportConnectionDstv('C1');
files[0].name; // 'C1-P1.NC' - the DSTV interface is ONE FILE PER PIECE
files[0].text; // ST header, AK external contour, BO holes, EN
skipped; // [{ kind, role, reason }] - the members and welds, and why they are not hereskipped is part of the answer, not decoration: a joint's own cut parts are its plates, and a MEMBER is cut from the frame (its section, its length, its saw cuts), so it is not in this export. Holes are the COMPLETED hole diameter (AS 4100:2020 Cl 14.3.2), including the 6 mm erection allowance in a base plate. In the designer, the same export is Joint > Export NC / DSTV, which downloads the single .NC for a one-plate joint and a zip of them for a several-plate one.
CivilKit.fabBom(opts?)
The fabrication bill of materials: every plate, bolt and weld the model's joints need, grouped, with lengths and masses.
CivilKit.exportModelIfc()
Returns the full structural model as an IFC4 string (members as IfcBeam / IfcColumn with real section profiles). Opens in Revit / Tekla / ArchiCAD.
js
const ifc = CivilKit.exportModelIfc();CivilKit.exportJointsIfc()
Returns the connections as an IFC4 string (joint assemblies + the Pset_CivilKit_JointTakeoff property set) - the fabricator/estimator deliverable. Run autoDesignConnections() first if you want joints in it.
js
const jointsIfc = CivilKit.exportJointsIfc();CivilKit.exportJson()
Returns the current model as a JSON string (the same data as getModel(), serialised - analysis/round-trip ready).
js
const json = CivilKit.exportJson();
// save it, then later: CivilKit.loadModel(JSON.parse(json));CivilKit.exportDxf()
Returns the model geometry as a DXF (CAD) string (members as LINE entities).
js
const dxf = CivilKit.exportDxf();Diagnostics
Read-only model-health queries. Unlike the toolbar buttons of the same name, these return the lists (no selection/highlight side effects), so a script can inspect a model before solving.
CivilKit.diagnostics()
Every reason to doubt the last solve, as rows - the same list the Diagnostics results tab shows, from the same derivation. null before anything is solved.
js
const d = CivilKit.diagnostics();
d.rows.forEach(r => console.log(r.severity, r.title, '-', r.detail));
// error 1 member solved as something else - 1 cable member ... falls through to an ordinary BEAM.
console.log(d.checked, 'checks ran;', d.rows.length, 'fired');Each row is { id, severity, title, detail, count }. severity is a claim about the numbers: 'error' means a result is wrong (a member type the solver does not model, reactions that do not balance the applied load, self weight the solver never saw), 'warn' that the solver answered a slightly different question than the one you asked, 'info' a fact about the run. worst is the highest severity present, or 'none'.
checked is how many checks ran, so an empty rows can be told apart from a check that never looked. The four counts the shape has always carried - autoRestrained, conditioning, tcOnlyLinear, unmodelled, clean - are still there.
CivilKit.findInstabilities()
Returns the nodes likely to cause a singular stiffness matrix:
js
CivilKit.findInstabilities();
// [{ nodeId, reason: 'free rotation (...)' | 'collinear truss ...' | 'reaches no support ...' }, ...]This is a heuristic, not an exhaustive rank-deficiency finder - an empty list does not guarantee the model will solve.
CivilKit.findOrphanNodes()
Returns the nodes connected to no member or plate:
js
CivilKit.findOrphanNodes();
// [{ id, num }, ...]Events
Subscribe to Studio events from a script. Supported events:
'solved'- fired after a successful solve. The callback receives{ analType, dt }(analysis type and solve time in ms).'modelChanged'- fired after an edit lands an undo step (programmatic or interactive). The callback receives no detail.
CivilKit.on(event, cb)
Registers cb for event. Returns an unsubscribe function. Throws on an unknown event name.
js
const off = CivilKit.on('solved', ({ analType, dt }) => {
console.log(`solved (${analType}) in ${dt} ms`);
});
// later:
off();js
CivilKit.on('modelChanged', () => console.log('model edited; results are stale'));CivilKit.off(event, cb)
Removes a handler previously registered with on(event, cb) (pass the same event and cb). Returns true if a handler was removed. Either off(...) or the function returned by on(...) unsubscribes.
js
function onSolved() { /* ... */ }
CivilKit.on('solved', onSolved);
CivilKit.off('solved', onSolved);End-to-end example
js
// React to every solve (optional - logs analysis type + time).
CivilKit.on('solved', ({ analType, dt }) => console.log(`solved ${analType} in ${dt} ms`));
// 1. Load a sample and check its health BEFORE touching it.
await CivilKit.loadSample('steelPortalReal');
console.log('Instabilities:', CivilKit.findInstabilities());
console.log('Orphan nodes:', CivilKit.findOrphanNodes());
// 2. Edit the model: add a braced bay between two existing nodes.
const nodes = CivilKit.getNodes();
const base = CivilKit.addNode({ x: 6, y: 0, z: 0, restraint: 31 }); // new pinned base
CivilKit.addMember({ nodeA: nodes[0].id, nodeB: base.id }); // new brace
// 3. Validate, then solve.
const issues = CivilKit.validate();
if (issues.length) console.warn('Model issues:', issues);
const summary = await CivilKit.solve();
console.log('Solved:', summary);
// 4. Read the design results.
const utils = CivilKit.getMemberUtilisations();
const worst = utils.reduce((a, b) => (b.gov > (a?.gov ?? -1) ? b : a), null);
console.log(`Worst member: M${worst.memberId} u=${worst.gov.toFixed(2)} (${worst.status}, ${worst.code})`);
console.log('Reactions:', CivilKit.getReactions());
// 5. Design connections and export the deliverables.
const conns = CivilKit.autoDesignConnections();
console.log(`Designed ${conns.length} connections`);
const modelIfc = CivilKit.exportModelIfc();
const jointsIfc = CivilKit.exportJointsIfc();
const json = CivilKit.exportJson(); // re-loadable model snapshotStability & scope
This is a deliberately small surface. It wraps Studio's internals so your scripts keep working as the internals change. The much larger window.__fem object also exists, but it is internal and unstable (raw snake_case wasm functions, live State) - do not script against it.