Product How it works Channel, sheet, the record's run Architecture Espo on one side, the API on the other The model It proposes the mapping. It does not save. The run Cadence, and this morning's number Rules Owner, key, reject Sheets The ones Espo already has The field Who wins if the cell is empty The record How it enters and how it leaves What can go wrong Preview, read mode, the switch
Channels Channels File, table, call, outbound Recipes A mapping to review The trial The repository, an empty Espo
Nesso Why Nesso Not SQL, not a second CRM Who it is for Who has to trust the run Decisions The ones the code keeps The project The public repository
FAQ The trial
Italiano English

The key, the hash and the cursor

21 October 2026 • Nesso
Key, hash and cursor on the night accounts file.

This is the second of four readings of the same run. The first put Espo on one side and the channel on the other. It left the key, the hash and the cursor for this chapter. The third will take the empty cell and the record that is closed. The fourth will say what stays off. Here the question is how a row is recognised, how the engine remembers that it has already seen the row, and how it remembers how far an outbound send has gone.

The file is the same one as in the first reading. It is the night accounts file. When it has the columns that are expected, C014 is unchanged and Espo is not called. C001 is new: the name is Nord Est, the city is Trieste, and the key ends with C001. When a required column is missing at the eighteenth row of the story, the file does not move. These are different passes. They stay separate, and further down each one is taken on its own.

If a sentence of this journal and a sentence of the manual diverge, the manual wins. The manual lives in the docs folder of the public repository github.com/nessocrm/nessocrm, and it changes with the code. This site does not recompile it. The journal explains why. The manual remains the text that says what the program does today.

The key

The identity of an inbound record is the integration id, a colon, and the external value. The name is not the identity. The city is not the identity. The id Espo assigns to the record, after creating it, is not the value the night file already knew. The string is composite, and the two parts have two origins. The integration id belongs to the rule that is reading. The external value comes on the row, in the column a person has marked as the identity.

In tonight’s file the external value of the new row is C001. The key written on the sheet ends with C001. Before the colon is the id of this integration, the accounts one. It is not an id of the connection, good for every rule attached to the same channel. It is not the id of another integration that happens to read a column of codes as well. The form is this one, and the page does not offer another. The colon holds the two parts together, so that a person who opens the record can see which rule brought it in and which code the row carried.

The key is looked up and written. Inbound, the engine looks to see whether that string is already on a record of the sheet. If it is, and the row is not already stored identical, the run updates. If it is not, the run creates. The hash can stop the run still earlier: when the row is already the stored one, Espo is not called. The key answers one question. Which record is this. The hash answers another. Has this row already been seen like this.

Two files can carry the same code and remain two stories, because to the left of the colon the id changes with the integration. The first chapter anticipated the separation. Here the character that makes it is visible: the colon, with a real id on the left and the column value on the right. Without the id, C001 from one file and C001 from another file would be the same string. With the id, the whole string changes. Updating one leaves the other alone.

Where the key is stored

On standard sheets the field is called cExternalKey. Account, when it is the standard sheet Espo already has, keeps the key there. The name begins with a lowercase C because that is how a custom field sits on a sheet that already existed. For that sheet, Nesso uses that field. It does not set a second field beside it under another name. One key, one field, on the standard sheet.

On a sheet created by Nesso the field is called externalKey. The leading C is absent. The new sheet has already received from Espo the prefix C on the entity name. Fields do not receive a second prefix. The key field keeps the name it was given, externalKey, with no further C in front. The two spellings are not two policies. They are the consequence of who created the sheet. Espo, for the sheets it already had. Nesso, through the API, for the sheet that was not there before, and even then the prefix on the entity is added by Espo.

The two sheets can live in the same installation. A standard Account looks up and writes cExternalKey. A sheet born here looks up and writes externalKey. The engine does not copy one name onto the other. A person reading a record looks at the sheet, and the field name follows the sheet. The string inside the field has the same form in both cases: integration id, colon, external value. C001, on the standard sheet of the story, ends the key that lives in cExternalKey.

A sheet that was not there

If the sheet does not exist, Nesso can create it through the API. The singular and the plural are given. They are the two names the sheet is called by, one for the record and one for the list. Espo adds the prefix C. The name that remains in Espo begins with that C. Fields added to the new sheet do not receive a second prefix: they keep the names that were given. The key, on that sheet, is externalKey.

Giving only the singular would not be enough, and giving only the plural would not be enough either. Espo asks for both names, and the creation carries both. The prefix is not typed by hand in front of the singular, in the hope that Espo will recognise it. Espo adds it. A second prefix on the fields, added so as to match the C of the entity, would produce names the sheet must not have. The fields stay without that second C.

The sheet created here is an Espo sheet. Whoever has the team can see it. The engine’s SQLite file does not become the place where that sheet lives. The first chapter already set this border, and it matters here only so the key is not lost: on the new sheet too the key is in Espo, in the externalKey field, and the hash stays in the engine. Creating the sheet changes the name of the field. It does not change the form of the value. The value remains id, colon, external value.

An accounts file aimed at Account and a file aimed at a sheet created here do not swap the field name. They do not swap the integration id either. The first key ends in the Account’s cExternalKey. The second ends in the externalKey of the new sheet. If the external values match, the whole strings match only when the integration id matches as well, which is when it is the same rule. Two rules remain two keys.

Two rules on the same sheet

The first chapter left the case of two integrations that land on Account. The key is why they do not overwrite each other. The integration id sits inside the key. The first key contains the id of the first. The second key contains the id of the second. Even if the external value happens to be the same string, the whole key is different, because to the left of the colon there are two ids.

C001, in the story, belongs to the accounts integration. The key ends with C001 and begins with the id of that integration. Another integration can have a row whose external value is C001 as well. That row’s key ends the same way and begins differently. In Espo, updating one is not updating the other. The engine does not decide that the two C001 values are the same customer. The person who made two integrations made two identities. If one identity is wanted later, there will be one integration, with one column.

The connection can be the same, if the two files arrive on the same channel. It can be different. The direction, in either case, follows the connection the integration is attached to. Two inbound integrations on the same sheet each have their own hash. Two outbound integrations each have their own cursor. The key keeps them apart even when the sheet is one sheet and the external value, read alone, would look unique.

This is also why one connection with one rule is not the shape of two files. The first chapter said it from the side of the border. Here it shows from the side of the string. A single identity column would force the two codes into the same key, with the same id on the left. A code from the other file would be looked up, updated or created as if it belonged to this rule. Two ids keep two memories. The hash of C014 is not the hash of the other integration, even if tonight the two files arrive in the same folder.

Direction follows the channel

An inbound channel brings rows toward Espo. The inbound channels are the webhook, the database read with SELECT only, CSV, Excel, SFTP, FTPS, a PDF whose filename contains the company key, and a document. The SELECT-only database can be MySQL, MariaDB, Postgres, SQL Server or Oracle. The SELECT reads that database. Espo’s tables stay out of that read: toward Espo the path is the HTTP API, and the chapter returns to that point when the run writes.

The outbound channel is HTTP. The integration leaves Espo and sends the mapped fields. A CSV is inbound. An Excel file is inbound. An HTTP connection of this product is outbound. Changing direction means changing the connection. The first chapter already said the sentence. Here the sentence decides which memory is used. Inbound remembers with the hash. Outbound remembers with the cursor. An inbound integration does not consult the cursor to know whether C014 is unchanged. An outbound integration does not consult the hash of a file row to know whether a record has already been sent.

The PDF comes in when the filename contains the company key. It is inbound, as the document is. SFTP and FTPS carry files, and the direction is the connection’s direction. A webhook that arrives is inbound. The HTTP call Nesso makes outward, on the outbound connection, is outbound. Attaching the night-file integration to an HTTP connection would make it leave Espo. Attaching an outbound rule to a folder would make it read files. The mistake, if there is one, is in the choice of connection, and it is visible before the run. It is not a box, inside the same rule, that reverses the direction halfway.

The same installation can have both directions at once, on two integrations. The night file comes in. Another rule, on HTTP, goes out. They have different ids, so different keys, so different memories. The success of one does not update the cursor of the other, and the cursor of one does not mark the hash of the file. At 02:10 the inbound run can start, if that is its cadence. The outbound run starts on its own cadence. If one of the two is already in progress, the other can still start. The same one cannot.

The hash, inbound memory

The hash is the memory of the row already seen, for that integration and for that external value. If the row hash is already the one stored, Espo is not called. The write does not start. The record stays as it is. The row is in the file, the engine recognises it, and the call is not there.

If the key already exists in Espo and the row is not the one already stored, the run updates that record. If the key does not exist, the run creates the record. Create and update are the two writes. The hash already stored is the absence of the write. An identical second send does not duplicate. The second time the hash is there, Espo is not called, and a second record with the same key is not born.

The order is this. First the hash. Then, only if the row is not already stored like that, the key. Creating when the key is there would open a duplicate. Updating when the key is not there would have no record to write on. Skipping the call when the hash matches avoids rewriting every night the row that has not changed, only because the file was dropped in the folder again.

The hash lives in the engine, with the rest of the run’s memory. The key lives on the record, in the sheet’s field: cExternalKey if the sheet is standard, externalKey if Nesso created the sheet. The two places do not trade roles. Espo receives the key, when the run writes. The engine keeps the hash, even when the run does not write. Tonight’s C014 is the case in which the hash is already in the engine and nothing starts toward Espo.

A hash per integration means that the same row, read by another rule, has another memory. The other rule has another id, so another key, so another comparison. Sharing the hash between two integrations would silence the second only because the first had already seen an equal text. That is not the behaviour. Each rule remembers its own rows.

C014 and C001, when the file is the expected one

In tonight’s file, when the required columns are there, the row for code C014 is identical to the last time. The row hash is already the one stored. Espo is not called. There is no update to send, and there is no reason to touch the record. The file contains the row. The engine skips it as a call. The record in Espo stays the one it was.

The row for code C001 is not there yet. The run prepares it as a new Account. The name is Nord Est. The city is Trieste. The key that is born on the standard sheet is the id of this integration, a colon, and C001. It ends with C001. The field is cExternalKey. The path is creation, because the key does not exist. It is not an update of a record found by name. Nord Est is the name the row carries. The question of a similar record with another key is a different case, further down. In this pass C001 is new, and the run creates.

An identical second send of the same C001 row does not create a second Account. The hash is stored by then. Espo is not called. The key still ends with C001, and it is the same key, because the integration is the same and the external value is the same. A record whose key ends with C001 for this rule is not born a second time.

This is the night on which the file has the expected shape. The eighteenth row of the story belongs to another pass over the same file: a required column is missing, the file does not move, and the other rows are not let through halfway. C014 and C001 are seen when the file is the expected one. Row 18 is seen when it is not. They do not happen together in the same pass. If they did, either a truncated file would be loaded, or a good row would be thrown away because of a cell. The two rules are further down, separate. Here it is enough to know that the file’s story has more than one night, or more than one condition, and that each condition has its own gesture.

Creating C001 writes the key into the field. The mapped fields carry the name and the city: Nord Est, Trieste. A field that is not mapped does not receive an invented value to fill the sheet. Outbound, when the cursor’s turn comes, will in the same way send only the mapped fields. Inbound, the write toward Espo is the creation or the update of what the rule has named, plus the key in the sheet’s field. C014, unchanged, never reaches that write.

The cursor, outbound memory

Outbound, there is no file to compare, row by row, with an inbound hash. There is Espo. There is a point up to which the send has already succeeded. The cursor remembers the last modifiedAt and the Espo id. The two values together say how far the previous run got. A record whose modifiedAt is already behind that memory, and whose id is already included, does not leave again only because the scheduler woke up.

The read is in pages of 50. The run asks for a page of records beyond the cursor, works those records, and goes on. Fifty is the size of the page. It is there so the whole archive is not asked for at once. The id kept together with modifiedAt is there so records that share the same instant are not lost: the cursor knows the instant and knows the id, so a record with the same modifiedAt and an id still to send stays in the page’s work, and is not skipped because that time has already been seen.

Only mapped fields are sent. A field of the sheet that the integration did not put in the mapping does not enter the body that leaves. Outbound is not a copy of the Account. It is the list a person named. modifiedAt and the Espo id serve the cursor, so it knows where it is. What leaves toward the outside, in the body of the POST, is the mapped fields.

The cursor advances only after successful POSTs. A POST that did not succeed is not a point that has been passed. The next run still has that record to send, because the cursor has not moved past it. Advancing before the response would move the memory onto a send that did not happen. The record would stay out, and the engine would be sure it had already delivered it. That is why the advance comes after success, not after the page has been prepared.

A page of 50 can hold records that leave and, if a POST does not succeed, a point at which the cursor stops. The records whose POST succeeded can be remembered up to the last success. The record whose POST did not succeed stays in front of the cursor. The next run finds it. The whole page is not marked done at the moment the page was read. The read prepares. The successful POST advances.

If the external key is missing on the way out

If the external key is missing, the pull stops. The records that come after do not leave. They are not skipped so the rest of the page can continue. The gap is a hole in the identity of the record that was about to be sent. The outbound run stops there. The cursor does not step over that record to go and fetch the later ones. Those later records remain to be sent, further on, when the key is there and a run can pass beyond.

It is a caution different from the hash. The hash, inbound, silences a call because the row is already known. Here the cursor refuses to go forward because a record has no identity the outbound send would recognise it by. Sending the later ones and leaving the hole in the middle would make an outbound run that looks complete and is not. Stopping is the gesture. The journal of that run says what happened, in the language of the run. The records after are not already gone. They did not leave.

The key in question, outbound, is the external key of the record in Espo. It is the same form already seen: integration id, colon, external value, in the sheet’s field. If that value is missing on the record, the pull has no identity to send onward, and it stops. It does not invent an external value to unblock the page. The record without a key remains the stopping point. The records after wait.

This stop is not the retry queue. The queue stays off, and it belongs to the fourth chapter. The outbound run that stops for a missing key does not park the later records in a queue. It leaves them unsent. The cursor has not passed them. A later run, when the key is there, can resume from where the outbound run stopped, because the advance happens only after successful POSTs and the hole was not a successful POST.

The second run that exports zero

A second outbound run, if nobody in Espo touched those records, exports zero. modifiedAt has not moved forward. The cursor is already on the last modifiedAt and the last id that left. There is no new page to send. Zero is not a fault in the channel. It is the outbound memory doing its work. The first chapter said that mixing up the hash and the cursor is how an outbound run resends the same records forever. The second run, with the records still, shows the other face: it does not resend.

If someone in Espo modifies a record, modifiedAt changes. The cursor, stopped at the previous instant and the previous id, sees that record again on the next run. It leaves again, with only the mapped fields. Only after the successful POST does the cursor take in the new modifiedAt and the id. A touch on one record does not send the whole sheet out again. What is beyond the cursor leaves. The others, untouched, stay at zero in that run: they have no new modifiedAt to bring them forward.

The hash does not enter this count. The hash is inbound memory. The cursor is outbound memory. A CSV file does not advance the cursor of an HTTP integration. An outbound POST does not mark the hash of C014. They are two memories. The same installation can have both, on two integrations, and each remembers only its own direction. At seven in the morning, if the outbound run exported zero, the right sentence is that nobody touched those records after the last cursor. It is not the sentence that the night file was identical. That sentence belongs to C014, to the hash, to inbound.

Two memories, two questions

It is worth holding them side by side, once, without mixing them.

The hash asks: has this inbound row already been stored like this? If yes, Espo is not called. If no, the key is looked at. Key present, update. Key absent, create. An identical second send falls on the first answer and does not duplicate.

The cursor asks: is this Espo record beyond the last modifiedAt and the last id already sent with success? If no, it does not leave. If yes, it enters a page of 50 and leaves with the mapped fields. The cursor moves only after the successful POST. If the external key is missing, the question closes: the pull stops and the records after do not leave. A second run, with no touches in Espo, has the answer ready and exports zero.

Neither question replaces the other. Using the hash for outbound would silence a record modified in Espo, if someone confused the memory of the row with the memory of the send. Using the cursor for inbound would reread C014 as already sent or as never seen, according to a modifiedAt the night file is not. The file has columns. Outbound has modifiedAt and id. The memories stay each on their own direction.

The lock, the cadence and the clock, which come now, are not a third memory of the data. They are the memory of when a run may start. The hash and the cursor remain the memories of what has already been seen and of what has already left. A run that does not start does not consume the hash and does not move the cursor. A run that starts uses them according to the direction of the channel.

The same integration does not start twice

The same integration does not start twice together. The second is told that a run is already in progress. Two runs of the same id do not open, reading the same file and writing the same key. Two concurrent writes on the same rule would race two hash comparisons, or two cursor advances, toward the same record.

Two different integrations can run together. The id is different, the key is different, the memory is different. The accounts file and another rule do not wait merely because they are two. They wait only when the id is the same and already in progress. An inbound run toward Account and an HTTP outbound run can both be executing. Each holds its own lock. Each, at the end, updates only its own memory: hash on one side, cursor on the other.

The scheduler, if it finds the lock held on an id, skips that id. It does not write an error every thirty seconds. A held lock is an ordinary condition: a run already started is holding the integration. An error every thirty seconds would fill the journal with a sentence that does not describe a fault. The skip concerns that id. The other integrations, if their lock is free and their cadence is due, start.

A start asked for while the same integration is already in progress receives the notice: a run is already in progress. It does not become a second parallel run. When the first has finished, the lock is released. A new start can begin, and it will use the hash or the cursor left by the first. The scheduler, in the meantime, has not filled that id’s journal.

The scheduler’s skip and the notice to the second start are two ways of saying the same exclusion, aimed at two callers. The scheduler has no person waiting: it skips, and it stays quiet. The other start asked to begin: it receives the sentence that a run is already in progress. In both cases the second run of the same id does not begin. The hash is not computed twice in parallel. The cursor does not advance twice in parallel.

Cadence, pause, Run now

The cadence is off, or manual; or five minutes; or fifteen; or sixty; or a fixed time, such as 02:10. Off and manual stand on the same side: the scheduler does not wake that integration by itself on an interval. Five, fifteen and sixty are intervals. 02:10 is a time of day, not a duration of two hours and ten minutes. The night file sits well on a fixed time. A webhook sits well on an arrival. The cadence can stay manual if the arrival itself is what wakes the rule, and the webhook, if the integration is not on pause, can start it.

An integration on pause is skipped by the scheduler and by the webhook. At 02:10 the scheduler arrives, sees the pause, and does not start. A webhook that knocks during the pause does not start that integration. Run now is not skipped. A person who asks for the run gets it even with the integration on pause. The pause stops the starts that do not have a hand on them. It does not stop the hand.

The pause does not clear the hash and does not put the cursor back to zero. It leaves them where they are. A Run now during the pause uses the same memory. Unchanged C014 does not call Espo. An outbound run does not resend the records already included in the cursor only because the rule was on pause. The pause is a skip of automatic starts. It is not a forgetting of the previous run.

Pause and lock add together without being confused. The pause skips the scheduler and the webhook even if no run is in progress. The lock skips the id, or tells the second start, when a run is in progress, even if the integration is not on pause. Run now passes through the pause. Run now, if the lock is held, is told that a run is already in progress. They are two gates. One is the pause. The other is the run already open. Both are looked at before believing that at 02:10 it starts anyway.

The fifteen minutes and the fake clock

The fifteen minutes are proved with a fake clock. One second early the run does not start. At the deadline it starts once. It does not start again in the same minute. It is not a quarter of an hour watched on the container, with the wall clock and a real wait. The container has its own time, and a quarter of an hour watched from outside mixes the process delay with the rule. The fake clock puts the instant where it is needed. The rule answers on that instant.

Take the deadline of the fifteen minutes. One second before, the cadence is not complete and the run stays still. At the instant of the deadline the condition is true and the run starts once only. Another instant, still inside the same minute as the deadline, does not produce a second start. The minute of the deadline is not a window to fill with starts. It is the minute in which the rule has already said yes, and it does not say it again until a later minute in which the cadence is complete once more.

The proof concerns the fifteen minutes. Five minutes, sixty minutes and a fixed time such as 02:10 are the same family of decisions, with another interval or with a time of day. This chapter does not turn the fifteen-minute proof into a story on the container for each cadence. It says where the proof is, and what it showed. Before, no. At the deadline, yes, once. In the same minute, no.

The fake clock does not move the hash and does not move the cursor. It moves the instant at which the decision to start is made. If at the deadline the lock of that id is held, the scheduler skips the id and does not write an error every thirty seconds, even if the cadence would be complete. If the integration is on pause, the scheduler skips it and the webhook would skip it. The deadline is necessary for the automatic start on an interval. It is not sufficient on its own, if another rule of this chapter says not to start.

After an error, the whole interval

After an error the clock updates, and the whole interval is waited. A failed run on a fifteen-minute cadence does not start again a minute later out of insistence. The memory of the last attempt moves onto the error, and the interval begins again from there. The error is not an invitation to call Espo continuously. It is an invitation to read the journal, which stays in the language of the run.

Only if the setting retry_failed is 1 does the clock stay put. In that case the attempt does not consume the interval. The choice is the value 1. Without that value, the clock updates and the whole interval is waited. The fourth chapter talks about what stays off. Here this value comes in only because it changes the clock of the next attempt. It is not the ordinary way of chasing an error, and it is not the retry queue. The queue stays off. The clock that updates is something else: it is the time that passes before the scheduler takes that id into consideration again.

An error on one integration does not move another integration’s clock. Cadence, lock and error are per id. Two integrations that run together can finish in two ways. One updates its own clock because it failed, and from there it waits the whole interval. The other succeeded, and its clock follows the success. The next time, the scheduler treats them according to their clocks. If a lock is still held, it skips that id without writing an error every thirty seconds.

On the night file, an error at 02:10 with a fixed-time cadence is the time that was attempted. With a cadence in minutes, the error starts the wait for the whole interval, unless retry_failed is 1. In both tellings the journal keeps the sentence of the error, in the language of the run. The home, in the morning, does not turn that failure into yesterday’s success. That belongs to the paragraph on the number.

The percentage on the home

The accounts percentage on the home counts inbound integrations toward Account, and only if today’s run succeeded. An inbound run toward another sheet does not enter that count. An HTTP outbound run, even if it reads Account, is not an inbound run toward Account, and it is not that number. The number looks at the direction and the sheet. It looks at the day.

Yesterday’s success is not shown as today. If tonight’s run did not succeed, the percentage does not fill itself with yesterday’s memory. The day is the day of the run. An inbound integration toward Account that finished well yesterday, and has no success today, does not lend its yesterday to this morning’s home. Whoever looks at the number in the morning is looking at today.

If today’s run succeeded, that inbound integration toward Account enters the count. Unchanged C014 and created C001, in the pass where the file was the expected one, belong to a run that can be a success of today: Espo was called for the creation, it was not called for the unchanged row, and the run went through. The number on the home does not separate, on the percentage, the row skipped for the hash from the row created. It separates the day, the direction and the sheet. The detail of the rows is in the journal.

A run that failed today, even if yesterday the same integration had created or updated, does not keep today’s percentage lit. Another integration, a different one, inbound toward Account, enters the count only if its run today succeeded. Two inbound runs toward Account are two ids. The number counts them according to each one’s success today, not according to an average success of the night.

The border, the processed folder, the notice

A file expected within an hour turns the border red if that hour passes without a successful run. The wait is the hour’s. Once the hour has passed, without a success, the border is red. A success inside the hour is not that border. The border is a check, like the percentage. It does not write to Espo. It does not move the file. It does not advance a cursor. It says that the hour has passed and the successful run is not there.

A file already in the processed folder is not read again. The run that brought it there already had it among the files to read. Reading it again would mean treating again a file that has already passed. The product does not put it back among the files of the run. The folder is the place of what has already been read. The hash, beside it, is the place of what has already been seen as a row. A file sitting in the processed folder does not recompute C014 and does not create C001 again.

If a required column is missing, the file is not moved. It stays where it arrived. It is not in the processed folder, and so it does not fall under the rule of what is not read again. The next run can still find it in the arrival folder. It is the case of the eighteenth row of the story. The reject is a single one, at file level. It is not a stack of rejects, one per row. It is not a file parked in the processed folder as if the read had been completed.

A failed-run notice lights once, with the integration name. It is not a new line every thirty seconds. The scheduler, faced with a lock, stays quiet at that pace. The failure notice, when it is there, lights once and carries the name, so it is clear which rule did not go through. A second integration, successful the same night, does not replace that name with its own. The notice belongs to that integration. The percentage, if the failed integration was an inbound run toward Account, does not count that run as today’s success.

Missing column, missing cell

A required column is missing. The file is not moved. The reject is a single one, at file level. The other rows do not pass halfway. The file is not the one that was expected. Treating it as a list of good rows and bad rows would load an accounts set made of the columns that remained, and a moved file would not be read again. That is why the file stays put and the reject is one. The eighteenth row of the story is this case. A required column is missing. The file does not move. In that pass C001 is not created and C014 is not compared as far as a call: the rows do not continue halfway.

A cell is missing, and the column is there. That row is rejected. The others continue. The expected column is in the file. One row does not have the value. The rows that do have the value continue, according to the hash and according to the key. Hash already stored: Espo is not called. Key existing: update. Key absent: create. The row with the missing cell does not become a record in that run.

They are two errors. The first speaks of the file. The second speaks of the row. Row 18 is the first. It is not an empty cell inside a column that is present. The empty cell, when the column is there and the field has an owner, is the third chapter. Here the missing cell with the column present is only the distinction of the reject: one row stops, the others do not. The file-level reject for the missing column stops the whole file and leaves it where it is.

Whoever checks the next morning distinguishes the two journals. One file-level reject, a single one, and the file still in the arrival folder: a column was missing. One row rejected and the others gone ahead: the column was there, the cell was not. The red border, if the expected hour has passed without a successful run, can light on either failure of the run. The notice carries the integration name once. Today’s percentage does not count a run that did not succeed.

A similar record with another key

Espo can refuse the write because a similar record already exists, with another key. The row is rejected. It is not silently joined to the record already present. It does not replace that record’s key with this row’s key. The similar record stays where it is, with the key it had. This run’s row does not enter.

The integration can be told to adopt that record. Adopting means taking the record that caused the refusal and continuing on that one. It is not the default behaviour. By default the row is rejected. Adopting is an instruction on the rule. Until the rule carries it, the reject remains the reject. An equal name, on its own, is not an order to adopt. The order, if it exists, is said on the integration.

Nord Est, with the key that ends with C001, in the pass where C001 is new, is not this refusal. It is a creation, because the key was not there and the hash could not already be stored for a row never seen. The similar record with another key is the case in which Espo says no. The journal has the reject of that row, and if the error names a field, the journal reports that name. Adopting, on the day someone asks for it on the rule, is another configuration. It is read as such. It is not tonight’s run, if tonight the rule does not say it.

The reject of the similar record does not move a file which, for another reason, was not to be moved. If the problem is the missing required column, one stays with the file-level reject, a single one, and the file does not move. The similar record is an answer from Espo on a row that was being written. So it is a row that reached the call. C014, with the hash already stored, does not reach the call, and cannot receive this refusal. The refusal belongs to a row the engine tried to write.

Date, list, phone, the name of the field

An unreadable date does not invent a day. The engine does not choose today’s date, and it does not choose a convenient day, in order to let the row through. If the date cannot be read, it does not become a date. Dates are read in three formats. The first is day, month and year with a slash, as in 18/10/2026. The second is year, month and day with a hyphen, as in 2026-10-18. The third is day, month and year with a hyphen, as in 18-10-2026. A value that is not one of these three is not translated into a day chosen in its place.

A list field needs at least one value. A list with no values does not satisfy the field. The run does not insert an entry the row did not bring, so as to be able to say that the list is there. At least one value is the condition. Short of that, the list field is not filled on its own.

An Italian phone becomes +39. The number, in the Italian form, is rewritten with that prefix. Toward Espo goes the value with +39. The rewrite concerns the phone recognised as Italian. It is not a key, and it is not a hash. It does not move the cursor. It sits among the gestures that prepare the value, before the write, on the same level as the date read in one of the three formats.

An Espo error that names a field reports that name. The journal does not stop at a refusal without a subject. It says the field Espo named. The reader returns to the mapping and to the row with that name in hand. The name is the name from the error. If the refusal is the similar record with another key, and the error names the field, that name is in the journal. If the date cannot be read, there is no invented day to go and look for in Espo under another name.

These checks stand before, or at the moment of, the write. They do not replace the hash. A C014 row already stored does not get as far as having its date read again: Espo is not called. A row that changes, or that is born, passes through here. C001, new, with the name Nord Est and the city Trieste, passes through creation. If one of its values were an unreadable date, that date would not become a day. If one of its values were a list with no value at all, the list would not be filled in. If one of its values were an Italian phone, it would become +39. If Espo refused and named a field, the journal would report that name.

The empty external id

An empty external id is a file-level concern, for retry. It is not a row placed in a queue. It is not queued. The external value is missing. The right-hand side of the key is missing. There is no separate row to retry on its own. The problem sits at file level.

The retry queue stays off. It belongs to the fourth chapter, with what stays off. Here the sentence is enough: it stays off. The empty identity is not the way a row enters that queue. It does not enter. This chapter does not turn the queue on, and it does not put the empty identity on a retry list.

The file-level reject, when a required column is missing, is the gesture of row 18: the file does not move, the reject is one. The empty external id, as a concern for retry, sits on that plane, the plane of the file. It does not sit on the plane of a queue of rows. The third chapter will talk about the empty cell when the column is there. That is not the empty identity, and it is not this page. Mixing up the empty cell of some ordinary field with the empty external id would let two different errors pass as one. The identity is the right-hand side of the key. Empty, it is a problem of the file, for the purpose of retry, and not an entry in a queue.

The night, in the order the engine sees it

The file’s connection is inbound. It is not outbound HTTP. The integration points at Account. The key, on the standard sheet, will go in cExternalKey. The form is the id of this integration, a colon, the external value.

If the integration is on pause, the scheduler skips it and the webhook skips it. Run now does not. If a run of this same integration is already in progress, the second start is told that a run is already in progress, and the scheduler skips the id without writing an error every thirty seconds. Another integration, with another id, can be in progress at the same time.

The cadence can be 02:10, if it is a fixed time. It can be fifteen minutes. In that case the fake clock has already said how the deadline is judged: one second early it does not start, at the deadline it starts once, in the same minute it does not start again. It is not a quarter of an hour watched on the container. After an error the clock updates and the whole interval is waited. Only with retry_failed equal to 1 does the clock stay put.

On the file that has the expected columns, row C014 has the hash already stored. Espo is not called. Row C001 does not have the key yet. An Account is born. The name is Nord Est. The city is Trieste. The key ends with C001. An identical second send of that row does not duplicate: the hash is there, the call does not start.

On the condition of the eighteenth row a required column is missing. That pass does not create C001 and does not call Espo for C014. The file does not move. The reject is one, at file level. A file already in the processed folder, read under another condition, is not read again. If instead the column is there and the cell is missing, that row is rejected and the others continue.

If Espo refuses because a similar record exists with another key, the row is rejected. Telling the integration to adopt that record is not the default. An unreadable date does not invent a day: it is read only in the three formats, 18/10/2026, 2026-10-18 and 18-10-2026. A list needs at least one value. An Italian phone becomes +39. An Espo error that names a field reports that name. An empty external id remains a file-level concern, for retry, and not a row in a queue. The queue stays off.

Outbound, on another integration, the channel is HTTP. The cursor remembers the last modifiedAt and the Espo id. It reads pages of 50. It sends only mapped fields. It advances only after successful POSTs. If the external key is missing, the pull stops and the records after do not leave. A second run, if nobody in Espo touched those records, exports zero.

In the morning, the percentage on the home counts the inbound run toward Account only if today’s run succeeded. Yesterday is not shown as today. If the file was expected within an hour and that hour has passed without a successful run, the border is red. The failed-run notice lights once, with the integration name.

Only the HTTP API

Writes toward Espo go through the HTTP API. Reads toward Espo, when the run has to look at a sheet, go through there as well. Espo’s tables are not a channel. They are not queried with SQL to look for cExternalKey, and they are not updated with SQL to write the key. The key is written with the call. The update is a call. The creation is a call. If the hash is already stored, the call is not there.

The SELECT-only database, when that is the inbound channel, is the external database. It can be MySQL, MariaDB, Postgres, SQL Server or Oracle. The SELECT reads that side. It is not a query on Espo’s tables. The outbound cursor, on the opposite side, is not implemented by going and reading the tables: it remembers modifiedAt and id, asks for pages of 50, and sends the mapped fields over HTTP.

Nesso is not a second CRM. The accounts live in Espo. Nord Est, once created, is an Account there. In the engine live the hash, the cursor, the lock, the clock, the journal. There is not a second Account sheet to consult in place of Espo. The percentage on the home looks at whether today’s run, inbound toward Account, succeeded. It does not count a parallel archive.

Nesso is not a certified connector. The name of the system that dropped the file is not in the code. The night file is a file, with columns, on an inbound channel. The rule is the integration. If this journal and the manual diverge, the manual wins, in the docs folder of the repository github.com/nessocrm/nessocrm.

The journal stays in the language of the run

The journal of a run stays in the language of the run. If the run started from a page in Italian, the sentences stay in Italian even if the next day the page is in English. The journal is the record of that execution. It is not translated afterwards. The scheduler speaks the language it was set in. Whoever reads an error in the morning reads the sentence the engine wrote.

This journal, the one you are reading, exists twice, in Italian and in English, because it is an explanation. The two texts say the same facts. The run’s journal will not do the same. It will say what happened to C014, to C001, to row 18, to the outbound run that exports zero, in the language of the run. Looking in the run’s journal for the prose of this chapter is looking in the wrong place. In the run’s journal, when Espo names a field, that name is there. When the notice of a failed run lights, it lights once and carries the integration name.

The language of the journal does not change the hash and does not change the cursor. A run in Italian and a run in English, on two integrations, each remember their own rows and their own modifiedAt. Keys are not translated. C001 stays C001. Nord Est stays the name. Trieste stays the city. +39 stays the prefix of the Italian phone. The three date formats stay those three values, whatever the language of the page. The page chooses the language of the person who configures. The record of the run chooses the language of the person who started the run, and it keeps it.

What this chapter did not do

It did not say what happens when the cell arrives empty, the column is there, and the field has an owner. The third chapter does that, together with the record that is no longer in the file and is closed in Espo. It did not turn the retry queue on. It stays off, and it is matter for the fourth chapter, with the rest of what stays off. It did not move Espo to the other side of the channel. The key, the hash and the cursor sit on the memory of the run. Tonight’s file remains a file.

C014, in the pass where the file is the expected one, produced no call. C001 produced a creation, with the name Nord Est, the city Trieste, and a key that ends with C001, in the cExternalKey of the standard sheet. Row 18 kept the file still, with a single reject, at file level. The second outbound run, if nobody touched the records, exported zero. The fifteen minutes were judged with the fake clock, not with a quarter of an hour on the container. These are the gestures of this chapter. The two chapters after have others.

The first reading remains the border: Espo on one side, the channel on the other, the integration as the rule. This reading is the way the rule recognises, remembers and does not do the work again. Without the key, C001 and another C001 would be confused. Without the hash, the unchanged row would call Espo every night. Without the cursor, outbound would resend the same records, or would lose the ones that did not leave. With the three, tonight’s run has a gesture for each row, and a gesture for outbound, and in the morning the home can say only what today really succeeded.

The trial is the repository

There is no form. The code is public and the trial starts with an empty Espo.

github.com/nessocrm/nessocrm

The passage

Espo on one side. The external system on the other.

Nesso stands in the middle. It reads the channel, applies the mapping, and writes through Espo’s API. It does not open the CRM tables.

01

The external system

Not us

A file, a table, a call. That ERP’s name is not in the code. Nesso is not that system.

02

Nesso

This is us

Channel, sheet, fields. If the write switch is off, it does not write. If the field belongs to Espo, it does not clear it.

03

Espo

The sheet

The record, if it is born, is born there. CRM mail stays in Espo. The Advanced Pack is not rewritten.

Nesso always has Espo on one side. It is not a bridge between any two systems.

The product boundary