SimpleORM Skills
Extend your DAO with reusable, composable behaviors that run automatically before or after CRUD operations.
Introduction
Skills are self-contained units of behavior that attach to iSimpleDAO<T> and execute automatically at specific points in the CRUD lifecycle. Each skill implements the iSimpleSkill interface, declares when it should run via TSkillRunAt, and receives full context (database query, AI client, logger, entity name, and operation) at execution time.
The iSimpleSkill Interface
iSimpleSkill = interface
['{C3D4E5F6-A7B8-9012-CDEF-123456789012}']
function Execute(aEntity: TObject; aContext: iSimpleSkillContext): iSimpleSkill;
function Name: String;
function RunAt: TSkillRunAt;
end;
TSkillRunAt
Determines when the skill fires during a CRUD operation:
| Value | Fires |
|---|---|
srBeforeInsert | Before INSERT is executed |
srAfterInsert | After INSERT is executed |
srBeforeUpdate | Before UPDATE is executed |
srAfterUpdate | After UPDATE is executed |
srBeforeDelete | Before DELETE is executed |
srAfterDelete | After DELETE is executed |
Fluent API via .Skill()
Skills are registered on the DAO using the fluent .Skill() method. You can chain as many skills as needed:
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillLog.New('PRODUCTS', srAfterInsert))
.Skill(TSkillTimestamp.New('CreatedAt', srBeforeInsert))
.Skill(TSkillValidate.New(srBeforeInsert));
Execution Context
Every skill receives an iSimpleSkillContext that provides access to shared resources:
iSimpleSkillContext = interface
function Query: iSimpleQuery; // Database connection
function AIClient: iSimpleAIClient; // AI provider (may be nil)
function Logger: iSimpleQueryLogger; // Logger instance (may be nil)
function EntityName: String; // Table name from [Tabela] attribute
function Operation: String; // 'INSERT', 'UPDATE', or 'DELETE'
end;
Creating Custom Skills
To create a custom skill, implement iSimpleSkill on a class that inherits from TInterfacedObject. Follow the standard TSimpleXxx.New() constructor pattern:
type
TSkillSendEmail = class(TInterfacedObject, iSimpleSkill)
private
FRecipient: String;
FRunAt: TSkillRunAt;
public
constructor Create(const aRecipient: String; aRunAt: TSkillRunAt = srAfterInsert);
destructor Destroy; override;
class function New(const aRecipient: String;
aRunAt: TSkillRunAt = srAfterInsert): iSimpleSkill;
function Execute(aEntity: TObject; aContext: iSimpleSkillContext): iSimpleSkill;
function Name: String;
function RunAt: TSkillRunAt;
end;
implementation
constructor TSkillSendEmail.Create(const aRecipient: String; aRunAt: TSkillRunAt);
begin
FRecipient := aRecipient;
FRunAt := aRunAt;
end;
destructor TSkillSendEmail.Destroy;
begin
inherited;
end;
class function TSkillSendEmail.New(const aRecipient: String;
aRunAt: TSkillRunAt): iSimpleSkill;
begin
Result := Self.Create(aRecipient, aRunAt);
end;
function TSkillSendEmail.Execute(aEntity: TObject;
aContext: iSimpleSkillContext): iSimpleSkill;
begin
Result := Self;
// Your custom logic here
SendNotificationEmail(FRecipient, aContext.EntityName, aContext.Operation);
end;
function TSkillSendEmail.Name: String;
begin
Result := 'send-email';
end;
function TSkillSendEmail.RunAt: TSkillRunAt;
begin
Result := FRunAt;
end;
Then register it on the DAO:
LDAO := TSimpleDAO<TPedido>.New(LQuery)
.Skill(TSkillSendEmail.New('admin@company.com', srAfterInsert));
Built-in Skills NEW
SimpleORM ships with 8 built-in skills in SimpleSkill.pas. These cover the most common cross-cutting concerns: logging, notification, auditing, timestamps, validation, referential integrity, history tracking, and webhooks.
TSkillLog
Logs operation details using the context Logger. If no Logger is assigned, falls back to OutputDebugString (Windows) or Writeln (console applications).
Constructor
class function New(const aPrefix: String = ''; aRunAt: TSkillRunAt = srAfterInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aPrefix | String | '' | Custom prefix appended to the log message |
aRunAt | TSkillRunAt | srAfterInsert | When the skill fires |
Output Format
[Skill:Log] PREFIX OPERATION ENTITY_NAME (TClassName)
Usage
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillLog.New('PRODUCTS', srAfterInsert))
.Skill(TSkillLog.New('PRODUCTS', srAfterUpdate))
.Skill(TSkillLog.New('PRODUCTS', srAfterDelete));
// After inserting a product, outputs:
// [Skill:Log] PRODUCTS INSERT PRODUTO (TProduto)
If you have a iSimpleQueryLogger attached via .Logger(), TSkillLog will use it. Otherwise it writes directly to the debug output or console.
TSkillNotify
Fires a TProc<TObject> callback after the operation completes, passing the entity as parameter. Useful for updating UI, triggering side effects, or broadcasting events.
Constructor
class function New(aCallback: TProc<TObject>; aRunAt: TSkillRunAt = srAfterInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aCallback | TProc<TObject> | — | Procedure to execute, receives the entity |
aRunAt | TSkillRunAt | srAfterInsert | When the skill fires |
Usage
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillNotify.New(
procedure(aEntity: TObject)
begin
Writeln('Product saved: ' + TProduto(aEntity).Nome);
end,
srAfterInsert
));
The callback is only executed if it is assigned (not nil). If you pass nil, the skill silently does nothing.
TSkillAudit
Writes an audit record to a database table every time an operation occurs. Records the entity name, operation type, and timestamp.
Constructor
class function New(const aAuditTable: String = 'AUDIT_LOG'; aRunAt: TSkillRunAt = srAfterInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aAuditTable | String | 'AUDIT_LOG' | Name of the audit table in the database |
aRunAt | TSkillRunAt | srAfterInsert | When the skill fires |
Required DDL
CREATE TABLE AUDIT_LOG (
ID INTEGER PRIMARY KEY,
ENTITY_NAME VARCHAR(100),
OPERATION VARCHAR(20),
CREATED_AT TIMESTAMP
);
Usage
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterInsert))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterUpdate))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterDelete));
This skill requires a valid iSimpleQuery in the context. If Query is nil, the skill silently exits without writing.
TSkillTimestamp
Automatically fills a TDateTime property on the entity with the current date/time (Now) using RTTI. Ideal for CREATED_AT and UPDATED_AT fields.
Constructor
class function New(const aFieldName: String; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aFieldName | String | — | Name of the Delphi property to fill (not the column name) |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
Usage
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillTimestamp.New('CreatedAt', srBeforeInsert))
.Skill(TSkillTimestamp.New('UpdatedAt', srBeforeUpdate));
The aFieldName parameter refers to the Delphi property name, not the database column name from [Campo]. For example, if your property is CreatedAt mapped to [Campo('CREATED_AT')], pass 'CreatedAt'.
TSkillValidate
Automatically calls TSimpleValidator.Validate on the entity before the operation. If validation fails, ESimpleValidator is raised and the operation is aborted.
Constructor
class function New(aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
Validated Attributes
| Attribute | Description |
|---|---|
[NotNull] | String must not be empty |
[NotZero] | Numeric value must not be zero |
[Format(max, min)] | String length between min and max |
[Email] | Must be a valid email address |
[MinValue(n)] | Numeric value >= n |
[MaxValue(n)] | Numeric value <= n |
[Regex('pattern', 'msg')] | Must match regular expression |
[CPF] | Valid Brazilian CPF number |
[CNPJ] | Valid Brazilian CNPJ number |
Usage
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillValidate.New(srBeforeInsert))
.Skill(TSkillValidate.New(srBeforeUpdate));
// If validation fails:
try
LDAO.Insert(LCliente);
except
on E: ESimpleValidator do
Writeln('Validation error: ' + E.Message);
end;
TSkillGuardDelete
Prevents deletion when dependent records exist in a child table. Runs a COUNT(*) query on the specified table and raises ESimpleGuardDelete if any records are found.
Constructor
class function New(const aTable, aFKField: String): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aTable | String | — | Child table name to check for dependent records |
aFKField | String | — | Foreign key column in the child table |
This skill has a fixed RunAt of srBeforeDelete. The RunAt value is not configurable via the constructor.
Usage
LDAO := TSimpleDAO<TCategoria>.New(LQuery)
.Skill(TSkillGuardDelete.New('PRODUTO', 'ID_CATEGORIA'));
// When trying to delete a category that has products:
try
LDAO.Delete(LCategoria);
except
on E: ESimpleGuardDelete do
Writeln(E.Message);
// "Cannot delete: 5 dependent records found in PRODUTO"
end;
The PK value is read from the entity using RTTI. The entity must have exactly one property decorated with [PK].
TSkillHistory
Saves a snapshot of all field values before changes are applied. Each field is recorded as a separate row in the history table. Useful for building audit trails and change logs.
Constructor
class function New(const aHistoryTable: String = 'ENTITY_HISTORY'; aRunAt: TSkillRunAt = srBeforeUpdate): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aHistoryTable | String | 'ENTITY_HISTORY' | Name of the history table |
aRunAt | TSkillRunAt | srBeforeUpdate | When the skill fires |
Required DDL
CREATE TABLE ENTITY_HISTORY (
ID INTEGER PRIMARY KEY,
ENTITY_NAME VARCHAR(100),
RECORD_ID VARCHAR(50),
FIELD_NAME VARCHAR(100),
OLD_VALUE VARCHAR(4000),
OPERATION VARCHAR(20),
CREATED_AT TIMESTAMP
);
Usage
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillHistory.New('ENTITY_HISTORY', srBeforeUpdate))
.Skill(TSkillHistory.New('ENTITY_HISTORY', srBeforeDelete));
Properties marked with [Ignore] are skipped. Only properties that have a [Campo] mapping are recorded. The field name stored is the column name from [Campo], not the Delphi property name.
TSkillWebhook
Sends a fire-and-forget HTTP POST request with the entity serialized as JSON. On failure, the error is logged but the operation is not blocked.
Constructor
class function New(const aURL: String; aRunAt: TSkillRunAt = srAfterInsert;
const aAuthHeader: String = ''): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aURL | String | — | Webhook endpoint URL |
aRunAt | TSkillRunAt | srAfterInsert | When the skill fires |
aAuthHeader | String | '' | Value for the Authorization header (e.g., 'Bearer token123') |
JSON Payload
{
"entity": "PRODUTO",
"operation": "INSERT",
"timestamp": "2026-03-10T14:30:00.000Z",
"data": {
"ID": 1,
"NOME": "Widget",
"PRECO": 29.90
}
}
Usage
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillWebhook.New(
'https://hooks.example.com/products',
srAfterInsert,
'Bearer my-secret-token'
));
Connection timeout is 5 seconds and response timeout is 10 seconds. If the webhook endpoint is unreachable or returns an error, a message is written to debug output or console, but the CRUD operation succeeds normally.
ERP Skills NEW
ERP skills address common business requirements: Brazilian document validation, sequential numbering, automatic calculations, stock movements, and installment generation. All are defined in SimpleSkill.pas alongside the built-in skills.
CPF/CNPJ Attributes
The [CPF] and [CNPJ] attributes provide automatic validation for Brazilian identity documents. They work as property-level attributes and are checked by TSimpleValidator.Validate (and by extension, TSkillValidate).
Entity Declaration
type
[Tabela('CLIENTE')]
TCliente = class
private
FId: Integer;
FCPF: String;
FCNPJ: String;
// getters and setters
published
[Campo('ID'), PK, AutoInc]
property Id: Integer read FId write FId;
[Campo('CPF'), CPF]
property CPF: String read FCPF write FCPF;
[Campo('CNPJ'), CNPJ]
property CNPJ: String read FCNPJ write FCNPJ;
end;
Usage
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillValidate.New(srBeforeInsert));
LCliente := TCliente.Create;
try
LCliente.CPF := '123.456.789-00'; // Invalid CPF
try
LDAO.Insert(LCliente);
except
on E: ESimpleValidator do
Writeln(E.Message); // CPF validation error
end;
finally
LCliente.Free;
end;
CPF/CNPJ validation accepts both formatted (with dots/dashes) and unformatted (digits only) input. The validator checks the mathematical check digits according to the official algorithm.
TSkillSequence
Generates automatic sequential numbers from a control table. Each series maintains its own counter independently. Ideal for invoice numbers, order numbers, or any sequential code.
Constructor
class function New(const aFieldName, aControlTable, aSerie: String): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aFieldName | String | — | Delphi property name to receive the number |
aControlTable | String | — | Name of the sequence control table |
aSerie | String | — | Series identifier for grouping sequences |
This skill has a fixed RunAt of srBeforeInsert. It always runs before insertion to ensure the number is set on the entity.
Required DDL
CREATE TABLE SEQUENCE_CONTROL (
ID INTEGER PRIMARY KEY,
SERIE VARCHAR(50) NOT NULL,
ULTIMO_NUMERO INTEGER NOT NULL DEFAULT 0
);
Usage
LDAO := TSimpleDAO<TNotaFiscal>.New(LQuery)
.Skill(TSkillSequence.New('Numero', 'SEQUENCE_CONTROL', 'NF'));
// First insert: Numero = 1
// Second insert: Numero = 2
// Third insert: Numero = 3
If the control table does not yet have a row for the specified series, the skill automatically inserts one starting at 1. Subsequent calls increment the existing counter.
TSkillCalcTotal
Automatically calculates a total field based on quantity, price, and optional discount. The formula is: Total = Qty * Price - Discount, rounded to 2 decimal places.
Constructor
class function New(const aTargetField, aQtyField, aPriceField: String;
const aDiscountField: String = ''; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aTargetField | String | — | Property name for the calculated total |
aQtyField | String | — | Property name for the quantity |
aPriceField | String | — | Property name for the unit price |
aDiscountField | String | '' | Property name for the discount (optional) |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
Usage
LDAO := TSimpleDAO<TItemPedido>.New(LQuery)
.Skill(TSkillCalcTotal.New('Total', 'Quantidade', 'PrecoUnitario', 'Desconto', srBeforeInsert))
.Skill(TSkillCalcTotal.New('Total', 'Quantidade', 'PrecoUnitario', 'Desconto', srBeforeUpdate));
LItem := TItemPedido.Create;
try
LItem.Quantidade := 10;
LItem.PrecoUnitario := 25.50;
LItem.Desconto := 5.00;
LDAO.Insert(LItem);
// LItem.Total is now 250.00 (10 * 25.50 - 5.00)
finally
LItem.Free;
end;
All parameter names refer to Delphi property names, not database column names. If the discount field is omitted (empty string), the discount is treated as zero.
TSkillStockMove
Records stock movements in a dedicated table. When an entity is inserted (e.g., a purchase order item), a stock entry is created. The movement type depends on the RunAt: after-insert or after-update records as 'SAIDA' (outgoing), after-delete records as 'ENTRADA' (incoming/reversal).
Constructor
class function New(const aMoveTable, aProductField, aQtyField: String;
aRunAt: TSkillRunAt = srAfterInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aMoveTable | String | — | Name of the stock movement table |
aProductField | String | — | Property name for the product ID |
aQtyField | String | — | Property name for the quantity |
aRunAt | TSkillRunAt | srAfterInsert | When the skill fires |
Required DDL
CREATE TABLE STOCK_MOVEMENT (
ID INTEGER PRIMARY KEY,
PRODUTO_ID INTEGER NOT NULL,
QUANTIDADE NUMERIC(15,4) NOT NULL,
TIPO VARCHAR(10) NOT NULL, -- 'ENTRADA' or 'SAIDA'
ENTITY_NAME VARCHAR(100),
CREATED_AT TIMESTAMP
);
Movement Type Logic
| RunAt | Movement Type | Use Case |
|---|---|---|
srAfterInsert | SAIDA | New sale order item created |
srAfterUpdate | SAIDA | Quantity changed on existing item |
srAfterDelete | ENTRADA | Item cancelled, stock returned |
Usage
LDAO := TSimpleDAO<TItemVenda>.New(LQuery)
.Skill(TSkillStockMove.New('STOCK_MOVEMENT', 'IdProduto', 'Quantidade', srAfterInsert))
.Skill(TSkillStockMove.New('STOCK_MOVEMENT', 'IdProduto', 'Quantidade', srAfterDelete));
The quantity is stored as an absolute value (Abs). The direction (entry or exit) is determined by the movement type, not the sign of the quantity.
TSkillDuplicate
Automatically generates installment records after an entity is inserted. The total value is split evenly across the specified number of installments, with the last installment absorbing any rounding difference. Due dates are calculated at regular intervals from the current date.
Constructor
class function New(const aInstallmentTable, aTotalField: String;
aCount, aIntervalDays: Integer): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aInstallmentTable | String | — | Name of the installment table |
aTotalField | String | — | Property name for the total value |
aCount | Integer | — | Number of installments |
aIntervalDays | Integer | — | Days between each due date |
This skill has a fixed RunAt of srAfterInsert. It runs after insertion so the entity PK is available for linking installments.
Required DDL
CREATE TABLE INSTALLMENT (
ID INTEGER PRIMARY KEY,
ENTITY_ID INTEGER NOT NULL,
NUMERO INTEGER NOT NULL,
VALOR NUMERIC(15,2) NOT NULL,
VENCIMENTO DATE NOT NULL,
STATUS VARCHAR(20) DEFAULT 'ABERTO',
CREATED_AT TIMESTAMP
);
Usage
LDAO := TSimpleDAO<TVenda>.New(LQuery)
.Skill(TSkillDuplicate.New('INSTALLMENT', 'Total', 3, 30));
LVenda := TVenda.Create;
try
LVenda.Total := 100.00;
LDAO.Insert(LVenda);
// Creates 3 installments:
// #1: R$ 33.33 due in 30 days
// #2: R$ 33.33 due in 60 days
// #3: R$ 33.34 due in 90 days (absorbs rounding)
finally
LVenda.Free;
end;
If the total value is zero or negative, no installments are generated. The skill requires a valid iSimpleQuery in the context and reads the entity PK via RTTI.
AI Skills NEW
AI Skills leverage large language models to automatically enrich, translate, summarize, tag, moderate, validate, and analyze entity data. All AI skills are defined in SimpleAISkill.pas and require an iSimpleAIClient to be attached to the DAO via .AIClient().
Nil-safe behavior: Enrichment and analysis skills (TSkillAIEnrich, TSkillAITranslate, TSkillAISummarize, TSkillAITags, TSkillAISentiment) silently exit if AIClient is nil. Validation and moderation skills (TSkillAIModerate, TSkillAIValidate) raise ESimpleAIModeration if AIClient is nil, because they are security-critical.
// Attach an AI client to the DAO
LDAO := TSimpleDAO<TArticle>.New(LQuery)
.AIClient(TSimpleAIClientOpenAI.New('sk-your-api-key'))
.Skill(TSkillAIEnrich.New('Summary', 'Summarize: {Title}'));
TSkillAIEnrich
Generates content using a prompt template and writes the AI response to a target property. The template supports {PropertyName} placeholders that are resolved via RTTI from the entity at runtime.
Constructor
class function New(const aTargetField, aPromptTemplate: String;
aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aTargetField | String | — | Property name to receive the AI-generated content |
aPromptTemplate | String | — | Prompt template with {PropertyName} placeholders |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
Usage
// Generate a product description from its name and category
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAIEnrich.New('Descricao',
'Write a compelling product description for: {Nome}, category: {Categoria}',
srBeforeInsert));
// Generate SEO meta tags
LDAO := TSimpleDAO<TArticle>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAIEnrich.New('MetaDescription',
'Write an SEO meta description (max 160 chars) for an article titled: {Title}',
srBeforeInsert));
// Generate a slug from the title
LDAO := TSimpleDAO<TPost>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAIEnrich.New('Slug',
'Generate a URL-friendly slug for: {Title}. Return only the slug, lowercase, hyphens only.',
srBeforeInsert));
Placeholders are case-sensitive and must match the exact Delphi property name (not the column name). For example, {Nome} resolves to the Nome property value.
TSkillAITranslate
Automatically translates the content of one property and writes the translation to another property. The AI is instructed to return only the translation text, nothing else.
Constructor
class function New(const aSourceField, aTargetField, aTargetLanguage: String;
aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aSourceField | String | — | Property name containing the source text |
aTargetField | String | — | Property name to receive the translation |
aTargetLanguage | String | — | Target language name (e.g., 'English', 'Spanish') |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
Usage
// Translate product descriptions to multiple languages
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAITranslate.New('Descricao', 'DescricaoEN', 'English', srBeforeInsert))
.Skill(TSkillAITranslate.New('Descricao', 'DescricaoES', 'Spanish', srBeforeInsert))
.Skill(TSkillAITranslate.New('Descricao', 'DescricaoFR', 'French', srBeforeInsert));
If the source field is empty, the skill silently exits without calling the AI client.
TSkillAISummarize
Automatically generates a summary of a text field and writes it to a target property. Optionally limits the summary to a maximum number of characters.
Constructor
class function New(const aSourceField, aTargetField: String;
aMaxLength: Integer = 0; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aSourceField | String | — | Property name containing the full text |
aTargetField | String | — | Property name to receive the summary |
aMaxLength | Integer | 0 | Maximum characters for the summary (0 = no limit) |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
Usage
// Summarize article content for preview
LDAO := TSimpleDAO<TArticle>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAISummarize.New('Content', 'Preview', 200, srBeforeInsert));
// Unlimited summary
LDAO := TSimpleDAO<TTicket>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAISummarize.New('Description', 'Summary', 0, srBeforeInsert));
When aMaxLength is 0, the AI is asked to summarize without a character constraint. When a positive value is provided, the prompt specifies the maximum character count.
TSkillAIModerate
Sends content to the AI for moderation. If the content is deemed inappropriate, the operation is blocked by raising ESimpleAIModeration. This is a security-critical skill that requires a valid iSimpleAIClient.
Constructor
class function New(const aField: String;
const aPolicy: String = ''; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aField | String | — | Property name containing the content to moderate |
aPolicy | String | '' | Custom moderation policy. If empty, uses a general content policy. |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
AI Response Protocol
The AI must respond with exactly one of:
APPROVED— content is acceptable, operation proceedsREJECTED: reason— content is rejected, raisesESimpleAIModerationwith the reason
Usage (without custom policy)
LDAO := TSimpleDAO<TComment>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAIModerate.New('Content', '', srBeforeInsert));
try
LDAO.Insert(LComment);
except
on E: ESimpleAIModeration do
Writeln('Content rejected: ' + E.Message);
end;
Usage (with custom policy)
LDAO := TSimpleDAO<TReview>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAIModerate.New('Text',
'No profanity, no personal attacks, no spam links',
srBeforeInsert));
Unlike enrichment skills, TSkillAIModerate raises ESimpleAIModeration if AIClient is nil. This is by design: moderation must never silently pass when the AI is unavailable.
TSkillAIValidate
Validates the entire entity against a natural language business rule by sending all field values to the AI. If the data violates the rule, the operation is blocked.
Constructor
class function New(const aRule: String;
const aErrorMessage: String = ''; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aRule | String | — | Natural language validation rule |
aErrorMessage | String | '' | Custom error message. If empty, uses the AI reason. |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
AI Response Protocol
The AI must respond with exactly one of:
VALID— data satisfies the rule, operation proceedsINVALID: reason— data violates the rule, raisesESimpleAIModeration
Usage
// Validate business rules using natural language
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAIValidate.New(
'Price must be positive and name must not contain special characters',
'', // Use AI-generated error message
srBeforeInsert
));
// With custom error message
LDAO := TSimpleDAO<TEmployee>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAIValidate.New(
'Salary must be between minimum wage and 50000, department must be a valid department name',
'Employee data does not meet company policy',
srBeforeInsert
));
try
LDAO.Insert(LEmployee);
except
on E: ESimpleAIModeration do
Writeln(E.Message); // 'Employee data does not meet company policy'
end;
Like TSkillAIModerate, this skill raises ESimpleAIModeration if AIClient is nil. All entity fields (except [Ignore]) are sent to the AI, so avoid using this on entities with sensitive data unless your AI provider meets your security requirements.
TSkillAISentiment
Analyzes the sentiment of a text field and writes the result to a target property. The AI returns exactly one of three values: POSITIVO, NEGATIVO, or NEUTRO.
Constructor
class function New(const aSourceField, aTargetField: String;
aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
| Parameter | Type | Default | Description |
|---|---|---|---|
aSourceField | String | — | Property name containing the text to analyze |
aTargetField | String | — | Property name to receive the sentiment result |
aRunAt | TSkillRunAt | srBeforeInsert | When the skill fires |
Possible Return Values
| Value | Meaning |
|---|---|
POSITIVO | Positive sentiment |
NEGATIVO | Negative sentiment |
NEUTRO | Neutral sentiment |
Usage
LDAO := TSimpleDAO<TReview>.New(LQuery)
.AIClient(LAIClient)
.Skill(TSkillAISentiment.New('Comment', 'Sentiment', srBeforeInsert));
LReview := TReview.Create;
try
LReview.Comment := 'This product is absolutely amazing!';
LDAO.Insert(LReview);
Writeln(LReview.Sentiment); // 'POSITIVO'
finally
LReview.Free;
end;
Sentiment values are returned in Portuguese (POSITIVO, NEGATIVO, NEUTRO) for consistency with the SimpleORM convention. If the source field is empty, the skill silently exits.
Complete Example
This example demonstrates a full DAO pipeline combining built-in, ERP, and AI skills on a single entity:
uses
SimpleDAO, SimpleSkill, SimpleAISkill, SimpleQueryFiredac,
SimpleLogger, SimpleInterface;
var
LQuery: iSimpleQuery;
LDAO: iSimpleDAO<TProduto>;
LAIClient: iSimpleAIClient;
LProduto: TProduto;
begin
LQuery := TSimpleQueryFiredac.New(FDConnection1);
LAIClient := TSimpleAIClientOpenAI.New('sk-your-api-key');
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Logger(TSimpleQueryLoggerConsole.New)
.AIClient(LAIClient)
// Validation (before insert and update)
.Skill(TSkillValidate.New(srBeforeInsert))
.Skill(TSkillValidate.New(srBeforeUpdate))
// Timestamps
.Skill(TSkillTimestamp.New('CreatedAt', srBeforeInsert))
.Skill(TSkillTimestamp.New('UpdatedAt', srBeforeUpdate))
// Sequential numbering
.Skill(TSkillSequence.New('Codigo', 'SEQUENCE_CONTROL', 'PROD'))
// Auto-calculate total
.Skill(TSkillCalcTotal.New('Total', 'Quantidade', 'PrecoUnitario', '', srBeforeInsert))
// AI enrichment: auto-generate description
.Skill(TSkillAIEnrich.New('Descricao',
'Write a short product description for: {Nome}', srBeforeInsert))
// AI tags
.Skill(TSkillAITags.New('Descricao', 'Tags', 5, srBeforeInsert))
// AI content moderation
.Skill(TSkillAIModerate.New('Descricao', '', srBeforeInsert))
// Audit trail
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterInsert))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterUpdate))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterDelete))
// History before changes
.Skill(TSkillHistory.New('ENTITY_HISTORY', srBeforeUpdate))
// Stock movements
.Skill(TSkillStockMove.New('STOCK_MOVEMENT', 'Id', 'Quantidade', srAfterInsert))
// Referential integrity guard
.Skill(TSkillGuardDelete.New('ITEM_PEDIDO', 'ID_PRODUTO'))
// Webhook notification
.Skill(TSkillWebhook.New('https://hooks.example.com/products', srAfterInsert))
// Logging
.Skill(TSkillLog.New('PRODUCTS', srAfterInsert))
.Skill(TSkillLog.New('PRODUCTS', srAfterUpdate))
.Skill(TSkillLog.New('PRODUCTS', srAfterDelete));
// Now use the DAO normally
LProduto := TProduto.Create;
try
LProduto.Nome := 'Wireless Keyboard';
LProduto.Quantidade := 50;
LProduto.PrecoUnitario := 89.90;
LDAO.Insert(LProduto);
// Skills executed in order:
// 1. TSkillValidate (srBeforeInsert) - validates entity
// 2. TSkillTimestamp (srBeforeInsert) - sets CreatedAt
// 3. TSkillSequence (srBeforeInsert) - sets Codigo
// 4. TSkillCalcTotal (srBeforeInsert) - calculates Total
// 5. TSkillAIEnrich (srBeforeInsert) - generates Descricao
// 6. TSkillAITags (srBeforeInsert) - generates Tags
// 7. TSkillAIModerate (srBeforeInsert) - checks Descricao
// 8. --- INSERT SQL executes ---
// 9. TSkillAudit (srAfterInsert) - writes audit record
// 10. TSkillStockMove (srAfterInsert) - records stock entry
// 11. TSkillWebhook (srAfterInsert) - sends HTTP POST
// 12. TSkillLog (srAfterInsert) - logs operation
finally
LProduto.Free;
end;
end;
Exceptions
Skills can raise specific exceptions to block CRUD operations. All skill-related exceptions are shown below:
| Exception | Defined In | Raised By | When |
|---|---|---|---|
ESimpleValidator |
SimpleValidator.pas |
TSkillValidate |
Entity fails attribute-based validation ([NotNull], [NotZero], [Format], [Email], [MinValue], [MaxValue], [Regex], [CPF], [CNPJ]) |
ESimpleGuardDelete |
SimpleSkill.pas |
TSkillGuardDelete |
Attempting to delete an entity that has dependent records in a child table |
ESimpleAIModeration |
SimpleAISkill.pas |
TSkillAIModerate |
AI rejects content as inappropriate, or AIClient is nil |
ESimpleAIModeration |
SimpleAISkill.pas |
TSkillAIValidate |
AI determines entity data violates the business rule, or AIClient is nil |
Handling Exceptions
try
LDAO.Insert(LEntity);
except
on E: ESimpleValidator do
Writeln('Validation failed: ' + E.Message);
on E: ESimpleGuardDelete do
Writeln('Cannot delete: ' + E.Message);
on E: ESimpleAIModeration do
Writeln('AI rejected: ' + E.Message);
end;
Non-blocking skills like TSkillWebhook and TSkillLog catch their own exceptions internally and never propagate errors to the caller. The CRUD operation always succeeds regardless of webhook or logging failures.