Open-source library of backend patterns and helpers for Creatio development, shipped as a Creatio configuration package (source code schemas compiled inside the configuration, not an external-only DLL).
It removes boilerplate that gets re-implemented on almost every Creatio project: ESQ / data access, outbound & inbound integrations, entity eventing, platform access (settings, localization, DI), and cross-cutting concerns such as logging, retries and error handling.
- Creatio 8.2+
- Runs on a consumer instance under .NET Framework 4.8 or .NET 8 — the public API surface stays within the .NET Standard 2.0 API set so it works on both.
a1DevKit is a Creatio configuration package. Install it into a Creatio
8.2+ environment in one of two ways.
clio is the community command-line tool for Creatio.
# 1. Install clio (one-time). Requires the .NET SDK.
dotnet tool install clio -g
# 2. Register your Creatio environment (one-time).
clio reg-web-app my-creatio -u https://my-instance.creatio.com -l Supervisor -p ********
# 3. Install the package into that environment.
clio push-pkg a1DevKit.zip -e my-creatioReplace the URL, login and password with your instance's values.
a1DevKit.zip is the package archive from the latest release — or build one
from the source package with clio generate-pkg-zip packages/a1DevKit.
If you cloned this repository as a clio workspace, you can push it directly instead of packaging a zip:
clio push-workspace -e my-creatio- Download the
a1DevKit.zipfrom the latest release. - In Creatio, open the Application Hub.
- Install an application from file and upload the
.zip. - Confirm the installation and wait for the configuration to compile.
The Application Hub route expects the release to be packaged as a composable application (with
app-descriptor.json). A bare configuration package can instead be installed from System Designer → Installed applications → Install from file.
- Built on the platform, not around it. Each helper is a Creatio source
code schema that wraps the platform's own APIs (
EntitySchemaQuery,UserConnection,Common.Logging,Terrasoft.File.Abstractions, Polly) directly — no abstraction layer and no external framework to learn. A helper takes aUserConnectionand is used exactly where you would have written the raw platform call, so it stays idiomatic Creatio code. - ESQ-first, cloud-safe. The default data access path is
EntitySchemaQuery; low-levelSelect/DBExecutoraccess — which can be blocked on hardened/cloud instances — is isolated in a clearly labeled, "on-prem only" module. - One helper = one source code schema. Each helper lives in its own schema
under
packages/a1DevKit/Schemas, is documented with English XML-doc, and ships with a usage example.
| Area | Helper(s) |
|---|---|
| Logging | a1CustomLogger — drop-in ILog whose verbosity is switched at runtime by a system setting |
| Data access | a1ESQExtention — typed per-schema CRUD: get by Id/filter, batched ForEach over large tables, event-bypassing column updates |
| Serialization | a1EntitySerializer — Entity → JSON with lookup (Id + display value) and detail handling |
| Integration — outbound | a1IntegrationDTO + a1RESTClient (REST) and a1SOAPClient (SOAP): Polly per-attempt timeout/retry, plus request/response logging to a1IntegrationLog |
| File parsing | a1CSVHelper, a1ExcelHelper — CSV/Excel → JSON with a shared set of inputs (stream, Creatio file record, or IFile) |
| Case management | a1DCMHelper (+ two business-process user tasks) — start/cancel DCM cases for a record |
Planned for future versions (not yet included): inbound configuration-service
base class; typed system settings and feature-flag accessors; localization
helpers; EntityEventListener / ScriptTask bases; and standalone Result<T>,
guard-clause and retry-policy primitives.
Out of scope: Freedom UI / frontend components, data migrations, no-code artifacts.
Schema: packages/a1DevKit/Schemas/a1CustomLogger/a1CustomLogger.cs
A drop-in replacement for a logger obtained via
Common.Logging.LogManager.GetLogger(...) that is gated by a single system
setting instead of by comments in the code.
- Forgetting to reference
Common.Loggingis a common source of compile errors when adding logging to a Creatio class —a1CustomLoggeris defined once, in one schema, with the reference already resolved. - Debug/Trace instrumentation added during development normally has to be
commented out afterwards so it doesn't flood the log. With
a1CustomLoggerit can stay in the code permanently: verbosity is controlled by a system setting, so it can be turned on in production for a few minutes to diagnose an issue and turned back off — no redeploy, no restart.
Two types make up this helper:
a1LogLevelEnum— a static class holding the fixedGuididentifiers for each verbosity level (NoneId,FatalId,ErrorId,WarnId,InfoId,DebugId,TraceId), ordered from least to most verbose, plus the pure comparison logicIsEnabled(configuredLevelId, messageLevelId)that decides whether a message at a given level should be written.a1CustomLogger— implements the fullCommon.Logging.ILoginterface. EveryTrace/Debug/Info/Warn/Error/Fatalcall (and all of their overloads) is gated by the correspondingIs*Enabledproperty, which reads the current threshold from thea1LogLevelsystem setting on every call — so a change to the setting takes effect immediately. Calls toGlobalVariablesContext/ThreadVariablesContext/NestedThreadVariablesContextpass straight through, since they are not log writes.
Because a1CustomLogger implements ILog, it can be used anywhere a regular
Common.Logging.ILog is expected.
The a1LogLevel system setting is expected to be a Lookup pointing to a
Value List whose items carry exactly the Guid values defined in
a1LogLevelEnum. Create that Value List and the system setting once per
environment; a1LogLevelEnum's Guid constants must match the Value List
item identifiers. An unset or unrecognized setting value is treated as
NoneId (logging disabled) — a misconfigured setting fails closed instead of
flooding the log.
using Terrasoft.Configuration;
public class MyService {
private readonly ILog _log;
public MyService(UserConnection userConnection) {
_log = new a1CustomLogger(userConnection, "MyServiceLogger");
}
public void DoWork(string input) {
_log.InfoFormat("[DoWork] Started. input={0}", input);
_log.DebugFormat("[DoWork] Intermediate step completed. value={0}", input);
}
}Raising the a1LogLevel system setting to Debug or Trace reveals the
detailed calls above; leaving it at Info or Warn suppresses them without
touching this code.
Schema: packages/a1DevKit/Schemas/a1DCMHelper/a1DCMHelper.cs
Starts and cancels DCM (Dynamic Case Management) cases for a specific record — the two operations every "restart the case flow for this record" scenario needs, instead of re-implementing them per business process user task.
- Starting a DCM case is not a plain
Process.Runcall — the platform activates it by sending the case's start signal for the target record. Getting this right (finding the right enabledDcmSchema, building the signal options) is easy to get subtly wrong at each call site. - There is no public bulk API to cancel every running DCM process instance
for a record. Doing it correctly means querying the system process tables
(
SysEntityCommonPrcEl/SysProcessElementData/SysProcessData/SysSchema) with a parameterized query —a1DCMHelpercentralizes that once instead of leaving raw SQL copy-pasted across user tasks.
a1DCMHelper wraps a UserConnection and exposes two operations:
RunDcmCase(recordId, entitySchemaName, dcmSchemaName = null)— resolves the entity schema by name, finds the matching enabledDcmSchema(the first enabled one whendcmSchemaNameis omitted, or the one with that name), and starts it viaUserConnection.ProcessEngine.RunDcmProcess. ThrowsInvalidOperationExceptionwhen the entity schema or a matching enabled case cannot be found.CancelDcmCases(recordId)— looks up every currently running DCM process instance linked torecordId, regardless of which case schema started it or whether that schema is still enabled, and callsCancelExecution()on each. This is the low-level/on-prem-oriented path in the helper: it reads system process tables directly because no ESQ-friendly public API exists for a per-record bulk cancel.
using Terrasoft.Configuration;
var dcmHelper = new a1DCMHelper(userConnection);
dcmHelper.RunDcmCase(orderId, "Order"); // first enabled case for Order
dcmHelper.RunDcmCase(orderId, "Order", "OrderDcmCase"); // a specific case by name
dcmHelper.CancelDcmCases(orderId); // cancel every running case for the recordTwo ready-to-use business process user tasks build on a1DCMHelper so a
process designer can start or cancel a DCM case for the current record
without writing any code:
a1RunDCMForRecord_UserTask(packages/a1DevKit/Schemas/a1RunDCMForRecord_UserTask/a1RunDCMForRecord_UserTask.cs) — reads theRecordId,EntitySchemaNameandDcmSchemaNameprocess parameters and callsa1DCMHelper.RunDcmCase.a1CancelDCMForRecord_UserTask(packages/a1DevKit/Schemas/a1CancelDCMForRecord_UserTask/a1CancelDCMForRecord_UserTask.cs) — reads theRecordIdprocess parameter and callsa1DCMHelper.CancelDcmCases.
Both user tasks are thin wrappers — all the actual logic lives in
a1DCMHelper, so it stays covered by a single implementation instead of being
duplicated per user task.
Schemas: packages/a1DevKit/Schemas/a1IntegrationDTO/a1IntegrationDTO.cs,
packages/a1DevKit/Schemas/a1RESTClient/a1RESTClient.cs
Together these carry a single outbound HTTP call end to end and persist it to
the "Integration log (a1)" (a1IntegrationLog) entity.
Property names mirror the a1IntegrationLog entity's columns one-to-one:
| Property | Column | Set by |
|---|---|---|
Service |
a1Service |
constructor |
Endpoint |
a1Endpoint |
CreateRequest |
Request |
a1Request |
CreateRequest |
Response |
a1Response |
the HTTP client, after the call |
StatusCode |
a1StatusCode |
the HTTP client, after the call |
Success |
a1Success |
the HTTP client, after the call |
RetryCount |
a1RetryCount |
caller, before the call (default 0 — no retries) |
Timeout |
a1Timeout |
caller, before the call (default 100 seconds) |
LogRequest() persists all of the above as a new a1IntegrationLog record.
Logging failures are caught and written to the application log instead of
being thrown, so a broken audit write never fails the integration call it is
logging.
Executes the call described by an a1IntegrationDTO — Get/Post/Put/
Patch/Delete, each taking the DTO and an optional logRequest flag
(default false) — and returns the same DTO with Response/StatusCode/
Success filled in.
Every call is guarded by a Polly resilience policy (Creatio already ships
Polly.dll, so no new third-party dependency is introduced) built from the
DTO's own Timeout/RetryCount:
- Timeout is applied per attempt — each retry gets its own fresh timeout window rather than sharing one budget across all attempts.
- Retry triggers on transport-level failures (
HttpRequestException) and on a timed-out attempt, with exponential backoff (2, 4, 8... seconds) between attempts. A response that simply carries a non-success HTTP status code (e.g.500) is not retried — it is written to the DTO like any other response, since that is a normal outcome, not a transport failure.
Network failures that survive every retry are caught rather than thrown, so a
caller can always check dto.Success instead of wrapping every call in
try/catch; in that case dto.StatusCode is 0 and dto.Response holds the
exception message.
using Terrasoft.Configuration;
var dto = new a1IntegrationDTO(userConnection, "https://api.example.com");
dto.CreateRequest("/v1/orders", requestJson);
dto.RetryCount = 3; // up to 3 extra attempts on transport failure/timeout
dto.Timeout = 15; // 15 seconds per attempt
var restClient = new a1RESTClient();
restClient.Post(dto, logRequest: true);
if (dto.Success) {
// use dto.Response
}Schema: packages/a1DevKit/Schemas/a1SOAPClient/a1SOAPClient.cs
The SOAP counterpart to a1RESTClient — same a1IntegrationDTO in/out contract,
same Polly per-attempt-timeout/exponential-backoff-retry policy, same
"network failures are caught, not thrown" behavior. It differs only where SOAP
itself differs from a typical REST call:
- Always sends a POST with content type
text/xml—dto.Requestmust already be the complete SOAP envelope (this helper does not build XML for you). - Takes an optional
soapActionparameter and sends it as the SOAP 1.1SOAPActionheader, auto-quoting the value if it isn't already quoted (the spec requires the header value to be a quoted string). Omit it for services that don't require one (SOAP 1.2 typically encodes the action in the content type instead). - A SOAP fault is just a response with a non-success status code — written to the DTO like any other response, not retried, exactly like a REST 500.
using Terrasoft.Configuration;
var dto = new a1IntegrationDTO(userConnection, "https://api.example.com");
dto.CreateRequest("/soap/OrderService", soapEnvelopeXml);
var soapClient = new a1SOAPClient();
soapClient.Send(dto, soapAction: "http://example.com/GetOrder", logRequest: true);
if (dto.Success) {
// parse dto.Response as XML
}Known duplication: a1RESTClient and a1SOAPClient each carry their own
copy of the Polly policy-building and URI-building helpers rather than sharing
a base class. This was a deliberate call to keep each schema fully
self-contained (see "One helper = one source code schema"); if a third HTTP
transport is added later, that logic is worth extracting into a shared
internal type instead of copying it a third time.
Schemas: packages/a1DevKit/Schemas/a1CSVHelper/a1CSVHelper.cs,
packages/a1DevKit/Schemas/a1ExcelHelper/a1ExcelHelper.cs
Both convert tabular file content into a JSON array: the first row becomes the property names, every following row becomes one JSON object, all values read as plain strings (no type inference). Both expose the same three input overloads, so a caller doesn't need to care where the file is coming from:
| Overload | Use when |
|---|---|
Parse(Stream stream) |
you already have the file open (e.g. a web service upload) |
Parse(string tableName, Guid fileId) |
the file is stored in a Creatio file record — resolved via EntityFileLocator, so it works whether the file physically lives in the DB, S3, or the filesystem |
Parse(IFile file) |
you already resolved the Terrasoft.File.Abstractions.IFile yourself |
a1CSVHelper parses CSV by hand (an RFC 4180 tokenizer — handles quoted
fields with embedded commas/newlines and escaped "" quotes), no third-party
dependency needed.
a1ExcelHelper reads .xls/.xlsx via ExcelDataReader (a new
third-party dependency this package bundles as a NuGet PackageReference in
packages/a1DevKit/Files/a1DevKit.csproj — restored automatically at build
time, alongside System.Text.Encoding.CodePages which it needs to read
legacy code-page-encoded .xls files; both target netstandard2.0, so they
run on both .NET Framework 4.8 and .NET 8 per the runtime-portability rule)
and takes an additional zero-based sheetIndex parameter (default 0,
the first sheet). Once the raw rows are read, it converts them to JSON with
the exact same row-to-JSON logic as a1CSVHelper (a1CSVHelper.RowsToJArray,
shared rather than duplicated, since the two are explicitly meant to behave
identically beyond the file format itself).
using Terrasoft.Configuration;
var csvHelper = new a1CSVHelper(userConnection);
var rowsFromUpload = csvHelper.Parse(uploadedStream);
var rowsFromFileRecord = csvHelper.Parse("LeadFile", fileId);
var excelHelper = new a1ExcelHelper(userConnection);
var sheetRows = excelHelper.Parse("LeadFile", fileId, sheetIndex: 0);Schema: packages/a1DevKit/Schemas/a1EntitySerializer/a1EntitySerializer.cs
Serializes a Creatio Entity to JSON (JObject) for integration payloads,
audit logs, or API responses — without manually enumerating every column.
- Hand-writing
entity.GetColumnValue(...)for every column of every schema you want to expose as JSON is repetitive, and breaks silently whenever a column is added or renamed. - Lookup columns need special handling to get both the Id and a human-readable display value, which is easy to get backwards (see below).
Serialize(schemaName, recordId)loads the record via ESQ (AddAllSchemaColumns) and returns oneJObjectwith one property per column. Regular columns serialize to their raw value; lookup columns serialize as{ "Id": "...", "DisplayValue": "..." }— the Id comes fromentity.GetColumnValue(column.Name), the display text fromentity.GetColumnValue(column.DisplayColumnValueName).Ignore(params string[] columnNames)adds columns to a permanent skip list, on top of the built-in defaults (ProcessListeners,SysModifiedInSchemaUId,SysCreatedInSchemaUId) — use it for large blobs or sensitive columns you never want in the output.Serialize(schemaName, recordId, columnNames)— whitelist overload, serializes only the given columns instead of the whole schema.SerializeWithDetails(schemaName, recordId, detailColumns)— serializes the record, then adds one JSON array per related detail schema.detailColumnsmaps detail schema name → the detail's FK column that points back at the parent record.
using Terrasoft.Configuration;
var serializer = new a1EntitySerializer(userConnection);
serializer.Ignore("Photo", "Notes"); // skip large/sensitive columns
JObject order = serializer.Serialize("Order", orderId);
JObject orderWithLines = serializer.SerializeWithDetails("Order", orderId,
new Dictionary<string, string> { ["OrderLine"] = "OrderId" });Schema: packages/a1DevKit/Schemas/a1ESQExtention/a1ESQExtention.cs
Typed helper for a single entity schema — construct once with a schema name and reuse across multiple operations instead of repeating ESQ setup, null-checks and try/catch at every call site.
Most CRUD call sites in a Creatio backend repeat the same handful of steps
(build an ESQ or fetch an entity, check for null, wrap Save() in
try/catch...). a1ESQExtention wraps those once per schema.
Methods split into two groups:
- Entity-based (fires
OnSaving/OnSavedetc. event handlers):GetById/TryGetById(throwing vs. safe fetch by Id),TryGetByColumn,GetWithColumns(fetch only specific columns),GetColumnValue<T>(read a single column without loading the full entity),Create,Save,DeleteById. - SQL-based (bypasses event handlers, faster):
UpdateColumn/UpdateColumnsrun directUpdatestatements — choose these only when you deliberately want to skip entity events. - Collection:
GetByFilter(all records matching one column filter) andForEach(iterates all records — or a pre-configuredEntitySchemaQuery— in batches ofbatchSizerows, default5000, so a large table is never loaded into memory at once).
GetById, Create and GetColumnValue<T> throw a1ESQExtentionException
when the record isn't found or can't be saved; use the Try... variants
instead when a missing record is an expected outcome rather than an error.
using Terrasoft.Configuration;
var orders = new a1ESQExtention(userConnection, "Order");
var order = orders.TryGetById(orderId);
if (order == null) {
var newOrderId = orders.Create(new Dictionary<string, object> {
["Name"] = "New order",
["AccountId"] = accountId
});
}
orders.UpdateColumn(orderId, "StatusId", newStatusId); // bypasses OnSaving/OnSaved
orders.ForEach(entity => {
// process every Order record in batches of 5000
});Automated tests for every helper live in tests/.
Released under the MIT License — see LICENSE.