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:

ValueFires
srBeforeInsertBefore INSERT is executed
srAfterInsertAfter INSERT is executed
srBeforeUpdateBefore UPDATE is executed
srAfterUpdateAfter UPDATE is executed
srBeforeDeleteBefore DELETE is executed
srAfterDeleteAfter 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;
ParameterTypeDefaultDescription
aPrefixString''Custom prefix appended to the log message
aRunAtTSkillRunAtsrAfterInsertWhen 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;
ParameterTypeDefaultDescription
aCallbackTProc<TObject>Procedure to execute, receives the entity
aRunAtTSkillRunAtsrAfterInsertWhen 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;
ParameterTypeDefaultDescription
aAuditTableString'AUDIT_LOG'Name of the audit table in the database
aRunAtTSkillRunAtsrAfterInsertWhen 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;
ParameterTypeDefaultDescription
aFieldNameStringName of the Delphi property to fill (not the column name)
aRunAtTSkillRunAtsrBeforeInsertWhen 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;
ParameterTypeDefaultDescription
aRunAtTSkillRunAtsrBeforeInsertWhen the skill fires

Validated Attributes

AttributeDescription
[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;
ParameterTypeDefaultDescription
aTableStringChild table name to check for dependent records
aFKFieldStringForeign 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;
ParameterTypeDefaultDescription
aHistoryTableString'ENTITY_HISTORY'Name of the history table
aRunAtTSkillRunAtsrBeforeUpdateWhen 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;
ParameterTypeDefaultDescription
aURLStringWebhook endpoint URL
aRunAtTSkillRunAtsrAfterInsertWhen the skill fires
aAuthHeaderString''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;
ParameterTypeDefaultDescription
aFieldNameStringDelphi property name to receive the number
aControlTableStringName of the sequence control table
aSerieStringSeries 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;
ParameterTypeDefaultDescription
aTargetFieldStringProperty name for the calculated total
aQtyFieldStringProperty name for the quantity
aPriceFieldStringProperty name for the unit price
aDiscountFieldString''Property name for the discount (optional)
aRunAtTSkillRunAtsrBeforeInsertWhen 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;
ParameterTypeDefaultDescription
aMoveTableStringName of the stock movement table
aProductFieldStringProperty name for the product ID
aQtyFieldStringProperty name for the quantity
aRunAtTSkillRunAtsrAfterInsertWhen 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

RunAtMovement TypeUse Case
srAfterInsertSAIDANew sale order item created
srAfterUpdateSAIDAQuantity changed on existing item
srAfterDeleteENTRADAItem 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;
ParameterTypeDefaultDescription
aInstallmentTableStringName of the installment table
aTotalFieldStringProperty name for the total value
aCountIntegerNumber of installments
aIntervalDaysIntegerDays 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;
ParameterTypeDefaultDescription
aTargetFieldStringProperty name to receive the AI-generated content
aPromptTemplateStringPrompt template with {PropertyName} placeholders
aRunAtTSkillRunAtsrBeforeInsertWhen 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;
ParameterTypeDefaultDescription
aSourceFieldStringProperty name containing the source text
aTargetFieldStringProperty name to receive the translation
aTargetLanguageStringTarget language name (e.g., 'English', 'Spanish')
aRunAtTSkillRunAtsrBeforeInsertWhen 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;
ParameterTypeDefaultDescription
aSourceFieldStringProperty name containing the full text
aTargetFieldStringProperty name to receive the summary
aMaxLengthInteger0Maximum characters for the summary (0 = no limit)
aRunAtTSkillRunAtsrBeforeInsertWhen 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.

TSkillAITags

Extracts keywords/tags from a text field and writes them as a comma-separated string to a target property.

Constructor

class function New(const aSourceField, aTargetField: String;
  aMaxTags: Integer = 5; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
ParameterTypeDefaultDescription
aSourceFieldStringProperty name containing the text to analyze
aTargetFieldStringProperty name to receive comma-separated tags
aMaxTagsInteger5Maximum number of tags to generate
aRunAtTSkillRunAtsrBeforeInsertWhen the skill fires

Usage

LDAO := TSimpleDAO<TArticle>.New(LQuery)
  .AIClient(LAIClient)
  .Skill(TSkillAITags.New('Content', 'Tags', 5, srBeforeInsert));

// After insert, Tags might contain:
// "delphi, orm, database, programming, tutorial"

The result is a single string with comma-separated keywords. To use as an array, split by comma in your application code.

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;
ParameterTypeDefaultDescription
aFieldStringProperty name containing the content to moderate
aPolicyString''Custom moderation policy. If empty, uses a general content policy.
aRunAtTSkillRunAtsrBeforeInsertWhen the skill fires

AI Response Protocol

The AI must respond with exactly one of:

  • APPROVED — content is acceptable, operation proceeds
  • REJECTED: reason — content is rejected, raises ESimpleAIModeration with 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;
ParameterTypeDefaultDescription
aRuleStringNatural language validation rule
aErrorMessageString''Custom error message. If empty, uses the AI reason.
aRunAtTSkillRunAtsrBeforeInsertWhen the skill fires

AI Response Protocol

The AI must respond with exactly one of:

  • VALID — data satisfies the rule, operation proceeds
  • INVALID: reason — data violates the rule, raises ESimpleAIModeration

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;
ParameterTypeDefaultDescription
aSourceFieldStringProperty name containing the text to analyze
aTargetFieldStringProperty name to receive the sentiment result
aRunAtTSkillRunAtsrBeforeInsertWhen the skill fires

Possible Return Values

ValueMeaning
POSITIVOPositive sentiment
NEGATIVONegative sentiment
NEUTRONeutral 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:

ExceptionDefined InRaised ByWhen
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.